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:
|
||||
devices:read, images:read, images:delete, display:write, templates:read, render:write,
|
||||
jobs:read, jobs:write, history:read, settings:write, admin.
|
||||
0.1.0 seeds one admin token; provisioning scoped client rows is supported by middleware
|
||||
but multi-token management UI is not implemented. Use a trusted server-to-server
|
||||
The deployment token seeds an administrator. Administrators can create separate
|
||||
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.
|
||||
|
||||
## 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.
|
||||
No internal endpoints, headers, stacks, paths or secrets are returned.
|
||||
|
||||
## Future webhook
|
||||
## Webhooks
|
||||
|
||||
SQLite events and webhook_subscriptions reserve operation.completed/failed,
|
||||
display.changed, device.online/offline/low_battery events. 0.1.0 emits operation/display
|
||||
events only; no callback delivery, retry worker or registration API is implemented.
|
||||
Future deliveries should use signed payloads and bounded retries.
|
||||
OAuth, tenancy, RBAC management and generated SDKs are future work.
|
||||
Administrators register callbacks through POST /api/v1/webhooks. Allowed public
|
||||
HTTPS hosts must be configured explicitly. Durable callbacks cover
|
||||
operation.completed, operation.failed, display.changed and observed
|
||||
device.online/device.offline/device.low_battery transitions. Device observation
|
||||
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
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ def install_ai_images(app, service):
|
||||
}
|
||||
).encode(),
|
||||
limit=service.settings.max_bytes,
|
||||
timeout=180,
|
||||
)
|
||||
result = json.loads(raw)["data"]
|
||||
if len(result) != 1:
|
||||
|
||||
@@ -161,6 +161,7 @@ class Assets:
|
||||
body=None,
|
||||
limit=None,
|
||||
accepted_status=(200,),
|
||||
timeout=10,
|
||||
):
|
||||
limit = limit or self.settings.max_bytes
|
||||
parsed = urlsplit(url)
|
||||
@@ -193,12 +194,12 @@ class Assets:
|
||||
|
||||
class PinnedHTTPS(http.client.HTTPSConnection):
|
||||
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(
|
||||
sock, server_hostname=parsed.hostname
|
||||
)
|
||||
|
||||
connection = PinnedHTTPS(parsed.hostname, timeout=10)
|
||||
connection = PinnedHTTPS(parsed.hostname, timeout=timeout)
|
||||
try:
|
||||
connection.request(
|
||||
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('#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 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('#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++;
|
||||
|
||||
@@ -50,6 +50,7 @@ def test_generation_queued_once_and_never_displays(client, service, monkeypatch)
|
||||
operation = service.operation(first.json()["operation_id"])
|
||||
assert operation["status"] == "COMPLETED"
|
||||
assert len(calls) == 1
|
||||
assert calls[0]["timeout"] == 180
|
||||
assert json.loads(calls[0]["body"])["n"] == 1
|
||||
assert service.registry.get("living-room").client.uploads == 0
|
||||
assert "private-ai-key" not in client.get("/api/v1/settings/ai-images").text
|
||||
|
||||
Reference in New Issue
Block a user