Align integration contract and image generation validation
This commit is contained in:
@@ -17,8 +17,11 @@ Authorization: Bearer FRAME_FLOW_TOKEN. The bootstrap token is provided through
|
|||||||
client_id, name, permissions, enabled, created_at, last_used_at. Permissions:
|
client_id, name, permissions, enabled, created_at, last_used_at. Permissions:
|
||||||
devices:read, images:read, images:delete, display:write, templates:read, render:write,
|
devices:read, images:read, images:delete, display:write, templates:read, render:write,
|
||||||
jobs:read, jobs:write, history:read, settings:write, admin.
|
jobs:read, jobs:write, history:read, settings:write, admin.
|
||||||
0.1.0 seeds one admin token; provisioning scoped client rows is supported by middleware
|
The deployment token seeds an administrator. Administrators can create separate
|
||||||
but multi-token management UI is not implemented. Use a trusted server-to-server
|
scoped clients through Connections or POST /api/v1/clients. The returned token is
|
||||||
|
shown once; GET /api/v1/clients never returns tokens. DELETE /api/v1/clients/{id}
|
||||||
|
revokes access. For Ticktar, grant devices:read, templates:read, render:write and
|
||||||
|
display:write; add images:read or jobs permissions only when needed. Use a trusted server-to-server
|
||||||
connection; CORS is opt-in by exact origins, never wildcard by default.
|
connection; CORS is opt-in by exact origins, never wildcard by default.
|
||||||
|
|
||||||
## Discovery and capabilities
|
## Discovery and capabilities
|
||||||
@@ -86,13 +89,19 @@ The authenticated client is authoritative; source.client is optional metadata.
|
|||||||
History stores client_id/request_id/operation_id/device_id/action/result/timestamp.
|
History stores client_id/request_id/operation_id/device_id/action/result/timestamp.
|
||||||
No internal endpoints, headers, stacks, paths or secrets are returned.
|
No internal endpoints, headers, stacks, paths or secrets are returned.
|
||||||
|
|
||||||
## Future webhook
|
## Webhooks
|
||||||
|
|
||||||
SQLite events and webhook_subscriptions reserve operation.completed/failed,
|
Administrators register callbacks through POST /api/v1/webhooks. Allowed public
|
||||||
display.changed, device.online/offline/low_battery events. 0.1.0 emits operation/display
|
HTTPS hosts must be configured explicitly. Durable callbacks cover
|
||||||
events only; no callback delivery, retry worker or registration API is implemented.
|
operation.completed, operation.failed, display.changed and observed
|
||||||
Future deliveries should use signed payloads and bounded retries.
|
device.online/device.offline/device.low_battery transitions. Device observation
|
||||||
OAuth, tenancy, RBAC management and generated SDKs are future work.
|
runs at most once per minute; failed cloud queries do not imply offline.
|
||||||
|
|
||||||
|
X-Frame-Flow-Signature: sha256=<hex> is HMAC-SHA256 over the exact request body.
|
||||||
|
Signing secrets are shown once and stored in mode-0600 files. Verify signatures
|
||||||
|
and deduplicate event IDs. Delivery retries at most three times; events before
|
||||||
|
registration are not replayed. Polling Operations remains available.
|
||||||
|
OAuth, tenancy and generated SDKs are future work.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ def install_ai_images(app, service):
|
|||||||
}
|
}
|
||||||
).encode(),
|
).encode(),
|
||||||
limit=service.settings.max_bytes,
|
limit=service.settings.max_bytes,
|
||||||
|
timeout=180,
|
||||||
)
|
)
|
||||||
result = json.loads(raw)["data"]
|
result = json.loads(raw)["data"]
|
||||||
if len(result) != 1:
|
if len(result) != 1:
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ class Assets:
|
|||||||
body=None,
|
body=None,
|
||||||
limit=None,
|
limit=None,
|
||||||
accepted_status=(200,),
|
accepted_status=(200,),
|
||||||
|
timeout=10,
|
||||||
):
|
):
|
||||||
limit = limit or self.settings.max_bytes
|
limit = limit or self.settings.max_bytes
|
||||||
parsed = urlsplit(url)
|
parsed = urlsplit(url)
|
||||||
@@ -193,12 +194,12 @@ class Assets:
|
|||||||
|
|
||||||
class PinnedHTTPS(http.client.HTTPSConnection):
|
class PinnedHTTPS(http.client.HTTPSConnection):
|
||||||
def connect(self):
|
def connect(self):
|
||||||
sock = socket.create_connection((ip, 443), timeout=10)
|
sock = socket.create_connection((ip, 443), timeout=timeout)
|
||||||
self.sock = ssl.create_default_context().wrap_socket(
|
self.sock = ssl.create_default_context().wrap_socket(
|
||||||
sock, server_hostname=parsed.hostname
|
sock, server_hostname=parsed.hostname
|
||||||
)
|
)
|
||||||
|
|
||||||
connection = PinnedHTTPS(parsed.hostname, timeout=10)
|
connection = PinnedHTTPS(parsed.hostname, timeout=timeout)
|
||||||
try:
|
try:
|
||||||
connection.request(
|
connection.request(
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const fs=require('fs');const path=require('path');const assert=require('assert')
|
|||||||
await page.locator('#make-preview').click();await page.waitForFunction(()=>!document.querySelector('#save-local').disabled);checks++;
|
await page.locator('#make-preview').click();await page.waitForFunction(()=>!document.querySelector('#save-local').disabled);checks++;
|
||||||
await page.locator('#save-local').click();const uploaded=page.locator('.image-card').filter({hasText:uploadName+'.jpg'});await uploaded.waitFor();checks++;
|
await page.locator('#save-local').click();const uploaded=page.locator('.image-card').filter({hasText:uploadName+'.jpg'});await uploaded.waitFor();checks++;
|
||||||
await uploaded.locator('[data-delete]').click();await page.locator('#modal-confirm').click();await uploaded.waitFor({state:'hidden'});checks++;
|
await uploaded.locator('[data-delete]').click();await page.locator('#modal-confirm').click();await uploaded.waitFor({state:'hidden'});checks++;
|
||||||
await page.locator('[data-page="templates"]').click();await page.locator('.template-card').first().waitFor();assert(await page.locator('.template-card').count()===7);checks++;
|
await page.locator('[data-page="templates"]').click();await page.locator('.template-card').first().waitFor();assert(await page.locator('.template-card').count()===9);checks++;
|
||||||
await page.locator('#render-template').click();await page.locator('#template-image').waitFor({timeout:30000});assert(await page.locator('#display-template').isEnabled());checks++;
|
await page.locator('#render-template').click();await page.locator('#template-image').waitFor({timeout:30000});assert(await page.locator('#display-template').isEnabled());checks++;
|
||||||
await page.locator('#field-title').fill('Edited title');assert(await page.locator('#display-template').isDisabled());checks++;await page.locator('#save-template').click();await page.reload();await page.locator('#field-title').waitFor();assert(await page.locator('#field-title').inputValue()==='Edited title');checks++;await page.locator('#reset-template').click();await page.locator('#modal-confirm').click();await page.waitForFunction(()=>document.querySelector('#field-title')?.value!=='Edited title'&&document.querySelector('#field-title'));assert(await page.locator('#field-title').inputValue()!=='Edited title');checks++;await page.locator('#field-title').fill('Provenance UI title');await page.locator('#render-template').click();await page.locator('#template-image').waitFor();await page.locator('#display-template').click();await page.locator('#confirm-display').click();await page.locator('[data-page="history"]').click();await page.locator('tbody tr').first().waitFor();checks++;await page.locator('tbody tr button').first().click();await page.locator('#modal-cancel').click();checks++;await page.locator('[data-page="dashboard"]').click();await page.locator('#edit-current-template').waitFor({timeout:30000});await page.locator('#edit-current-template').click();await page.locator('#field-title').waitFor();assert(await page.locator('#field-title').inputValue()==='Provenance UI title');checks++;
|
await page.locator('#field-title').fill('Edited title');assert(await page.locator('#display-template').isDisabled());checks++;await page.locator('#save-template').click();await page.reload();await page.locator('#field-title').waitFor();assert(await page.locator('#field-title').inputValue()==='Edited title');checks++;await page.locator('#reset-template').click();await page.locator('#modal-confirm').click();await page.waitForFunction(()=>document.querySelector('#field-title')?.value!=='Edited title'&&document.querySelector('#field-title'));assert(await page.locator('#field-title').inputValue()!=='Edited title');checks++;await page.locator('#field-title').fill('Provenance UI title');await page.locator('#render-template').click();await page.locator('#template-image').waitFor();await page.locator('#display-template').click();await page.locator('#confirm-display').click();await page.locator('[data-page="history"]').click();await page.locator('tbody tr').first().waitFor();checks++;await page.locator('tbody tr button').first().click();await page.locator('#modal-cancel').click();checks++;await page.locator('[data-page="dashboard"]').click();await page.locator('#edit-current-template').waitFor({timeout:30000});await page.locator('#edit-current-template').click();await page.locator('#field-title').waitFor();assert(await page.locator('#field-title').inputValue()==='Provenance UI title');checks++;
|
||||||
await page.locator('[data-page="automation"]').click();await page.locator('#new-job').click();await page.locator('#job-content-type').waitFor();await page.locator('#job-template').selectOption('quote');checks++;await page.locator('#job-name').fill('UI acceptance');await page.locator('#job-form button[type=submit]').click();await page.locator('td strong').filter({hasText:'UI acceptance'}).waitFor();checks++;
|
await page.locator('[data-page="automation"]').click();await page.locator('#new-job').click();await page.locator('#job-content-type').waitFor();await page.locator('#job-template').selectOption('quote');checks++;await page.locator('#job-name').fill('UI acceptance');await page.locator('#job-form button[type=submit]').click();await page.locator('td strong').filter({hasText:'UI acceptance'}).waitFor();checks++;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ def test_generation_queued_once_and_never_displays(client, service, monkeypatch)
|
|||||||
operation = service.operation(first.json()["operation_id"])
|
operation = service.operation(first.json()["operation_id"])
|
||||||
assert operation["status"] == "COMPLETED"
|
assert operation["status"] == "COMPLETED"
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
assert calls[0]["timeout"] == 180
|
||||||
assert json.loads(calls[0]["body"])["n"] == 1
|
assert json.loads(calls[0]["body"])["n"] == 1
|
||||||
assert service.registry.get("living-room").client.uploads == 0
|
assert service.registry.get("living-room").client.uploads == 0
|
||||||
assert "private-ai-key" not in client.get("/api/v1/settings/ai-images").text
|
assert "private-ai-key" not in client.get("/api/v1/settings/ai-images").text
|
||||||
|
|||||||
Reference in New Issue
Block a user