@friggframework/core 2.0.0--canary.703.12b980b.0 → 2.0.0--canary.707.df80bfd.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-encryption-service.js +9 -22
- package/database/documentdb-utils.js +2 -2
- package/database/encryption/README.md +78 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +219 -0
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/database/organization-where.js +18 -0
- package/database/prisma.js +15 -38
- package/database/unique-violation.js +25 -0
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +230 -0
- package/extensions/index.js +44 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/prisma-composer.js +270 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +33 -42
- package/generated/prisma-mongodb/index-browser.js +30 -39
- package/generated/prisma-mongodb/index.d.ts +3187 -2005
- package/generated/prisma-mongodb/index.js +33 -42
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +86 -59
- package/generated/prisma-mongodb/wasm.js +33 -42
- package/generated/prisma-postgresql/edge.js +33 -42
- package/generated/prisma-postgresql/index-browser.js +30 -39
- package/generated/prisma-postgresql/index.d.ts +3380 -2177
- package/generated/prisma-postgresql/index.js +33 -42
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +81 -57
- package/generated/prisma-postgresql/wasm.js +33 -42
- package/handlers/WEBHOOKS.md +0 -29
- package/handlers/app-definition-loader.js +128 -19
- package/handlers/app-handler-helpers.js +27 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/integration-event-dispatcher.js +3 -7
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +100 -50
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +66 -16
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +1 -14
- package/integrations/integration-base.js +2 -163
- package/integrations/integration-router.js +132 -102
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +37 -9
- package/integrations/repositories/integration-repository-interface.js +17 -4
- package/integrations/repositories/integration-repository-mongo.js +64 -96
- package/integrations/repositories/integration-repository-postgres.js +63 -81
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +56 -31
- package/integrations/use-cases/delete-integration-for-user.js +4 -4
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integration-for-user.js +2 -4
- package/integrations/use-cases/get-integration-instance.js +4 -4
- package/integrations/use-cases/get-integrations-for-user.js +79 -39
- package/integrations/use-cases/get-possible-integrations.js +2 -7
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/use-cases/update-integration.js +4 -4
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +415 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-bases.js +169 -0
- package/modules/proxy/proxy-errors.js +31 -0
- package/modules/proxy/proxy-path.js +183 -0
- package/modules/proxy/proxy-policy.js +277 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/requester/proxy-request.js +245 -0
- package/modules/requester/requester.js +35 -5
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +331 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +329 -0
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -7
- package/prisma-mongodb/schema.prisma +86 -59
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +81 -57
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/queuer-util.js +2 -9
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +214 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/errors/mapping-already-exists-error.js +0 -14
- package/handlers/workers/integration-triggers.js +0 -25
- package/infrastructure/scheduler/recurrence.js +0 -204
- package/integrations/define/DESIGN.md +0 -122
- package/integrations/define/config.js +0 -98
- package/integrations/define/define-app.js +0 -66
- package/integrations/define/define-extension.js +0 -154
- package/integrations/define/define-integration.js +0 -335
- package/integrations/define/examples/deal-alerts.js +0 -140
- package/integrations/define/extension-binding.js +0 -174
- package/integrations/define/index.js +0 -11
- package/integrations/define/json-schema-lite.js +0 -68
- package/integrations/define/subscriptions.js +0 -186
- package/integrations/define/tests/fixtures/hubspot.js +0 -109
- package/integrations/define/tests/fixtures/slack.js +0 -26
- package/integrations/define/use.js +0 -39
- package/integrations/definitions/README.md +0 -278
- package/integrations/definitions/canonical.js +0 -11
- package/integrations/definitions/capabilities/AUDIT.md +0 -480
- package/integrations/definitions/capabilities/acmeerp.capabilities.json +0 -633
- package/integrations/definitions/capabilities/asana.capabilities.json +0 -279
- package/integrations/definitions/capabilities/calendly.capabilities.json +0 -385
- package/integrations/definitions/capabilities/docusign.capabilities.json +0 -314
- package/integrations/definitions/capabilities/fathom.capabilities.json +0 -238
- package/integrations/definitions/capabilities/github.capabilities.json +0 -585
- package/integrations/definitions/capabilities/hubspot.capabilities.json +0 -822
- package/integrations/definitions/capabilities/jira.capabilities.json +0 -391
- package/integrations/definitions/capabilities/klaviyo.capabilities.json +0 -503
- package/integrations/definitions/capabilities/linear.capabilities.json +0 -378
- package/integrations/definitions/capabilities/netsuite.capabilities.json +0 -535
- package/integrations/definitions/capabilities/notion.capabilities.json +0 -208
- package/integrations/definitions/capabilities/phone.capabilities.json +0 -511
- package/integrations/definitions/capabilities/pipedrive.capabilities.json +0 -430
- package/integrations/definitions/capabilities/quickbooks.capabilities.json +0 -635
- package/integrations/definitions/capabilities/salesforce.capabilities.json +0 -568
- package/integrations/definitions/capabilities/shopify.capabilities.json +0 -1128
- package/integrations/definitions/capabilities/slack.capabilities.json +0 -373
- package/integrations/definitions/capabilities/stripe.capabilities.json +0 -630
- package/integrations/definitions/capabilities/webflow.capabilities.json +0 -196
- package/integrations/definitions/capabilities/zendesk.capabilities.json +0 -422
- package/integrations/definitions/compile.js +0 -80
- package/integrations/definitions/config-form.js +0 -137
- package/integrations/definitions/examples/acme-erp-quickbooks.definition.json +0 -467
- package/integrations/definitions/examples/asana-slack.definition.json +0 -219
- package/integrations/definitions/examples/calendly-salesforce.definition.json +0 -459
- package/integrations/definitions/examples/docusign-salesforce.definition.json +0 -305
- package/integrations/definitions/examples/fathom-hubspot-notion.definition.json +0 -575
- package/integrations/definitions/examples/github-linear.definition.json +0 -322
- package/integrations/definitions/examples/hubspot-slack.definition.json +0 -427
- package/integrations/definitions/examples/klaviyo-hubspot.definition.json +0 -336
- package/integrations/definitions/examples/phone-hubspot.definition.json +0 -633
- package/integrations/definitions/examples/pipedrive-hubspot.definition.json +0 -136
- package/integrations/definitions/examples/pipedrive-phone.definition.json +0 -340
- package/integrations/definitions/examples/records/phone-hubspot.definition-version.json +0 -652
- package/integrations/definitions/examples/records/phone-hubspot.integration.json +0 -86
- package/integrations/definitions/examples/records/phone-hubspot.mappings.json +0 -39
- package/integrations/definitions/examples/records/phone-hubspot.workflow-run.json +0 -62
- package/integrations/definitions/examples/shopify-klaviyo.definition.json +0 -488
- package/integrations/definitions/examples/shopify-netsuite.definition.json +0 -622
- package/integrations/definitions/examples/stripe-quickbooks.definition.json +0 -476
- package/integrations/definitions/examples/webflow-hubspot-slack.definition.json +0 -262
- package/integrations/definitions/examples/zendesk-jira.definition.json +0 -604
- package/integrations/definitions/expressions.js +0 -229
- package/integrations/definitions/index.js +0 -48
- package/integrations/definitions/integration-runtime.js +0 -779
- package/integrations/definitions/interpreter.js +0 -104
- package/integrations/definitions/manifests.js +0 -177
- package/integrations/definitions/renderers/common.js +0 -67
- package/integrations/definitions/renderers/html.js +0 -129
- package/integrations/definitions/renderers/hubspot.js +0 -59
- package/integrations/definitions/renderers/preview.js +0 -154
- package/integrations/definitions/renderers/slack.js +0 -146
- package/integrations/definitions/repositories/definition-version-repository.js +0 -67
- package/integrations/definitions/repositories/workflow-run-repository.js +0 -80
- package/integrations/definitions/router.js +0 -170
- package/integrations/definitions/runtime.js +0 -268
- package/integrations/definitions/schema/capability-manifest.v0.1.schema.json +0 -549
- package/integrations/definitions/schema/installation.v0.1.schema.json +0 -130
- package/integrations/definitions/schema/integration-definition.v0.1.schema.json +0 -909
- package/integrations/definitions/schema/records.md +0 -105
- package/integrations/definitions/schema-check.js +0 -58
- package/integrations/definitions/scripts/check-manifests.js +0 -220
- package/integrations/definitions/scripts/validate.js +0 -66
- package/integrations/definitions/steps.js +0 -123
- package/integrations/definitions/sync-engine.js +0 -297
- package/integrations/definitions/tests/helpers-fake-crm.js +0 -49
- package/integrations/definitions/transforms.js +0 -91
- package/integrations/definitions/use-cases/definition-use-cases.js +0 -245
- package/integrations/definitions/validate.js +0 -436
- package/integrations/definitions/versioning.js +0 -176
- package/integrations/definitions/webhooks.js +0 -119
- package/integrations/integration-types.js +0 -73
- package/integrations/triggers.js +0 -244
- package/integrations/use-cases/run-trigger-tick.js +0 -155
- package/integrations/use-cases/run-trigger.js +0 -77
- package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +0 -66
- package/prisma-postgresql/migrations/20261019000000_workflow_run_waits/migration.sql +0 -7
- package/prisma-postgresql/migrations/20261019120000_definition_workspace/migration.sql +0 -5
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# defineIntegration: design note
|
|
2
|
-
|
|
3
|
-
ADR-054's stable, event-driven path (target file (a)) built on Stack B's integration runtime (ADR-063, #693–#700).
|
|
4
|
-
The same decisions are recorded as an amendment in ADR-054 (#669).
|
|
5
|
-
|
|
6
|
-
## 1. One runtime, two front doors
|
|
7
|
-
|
|
8
|
-
Both front doors compile to one target: an `IntegrationBase` subclass with a static `Definition` and an instance
|
|
9
|
-
`events` map. Everything downstream reads only that target: the devtools builders, the defined-routes and webhook
|
|
10
|
-
routers, the queue worker, the ADR-055 tick (`RunTriggerTick`, one tick, per-installation due decisions), the run
|
|
11
|
-
(`IntegrationBase#onTriggerRun` with `RunTrigger`), `integration-types` and the management API.
|
|
12
|
-
|
|
13
|
-
| | Code (`defineIntegration`, this folder) | Document (ADR-063, `definitions/`) |
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| Entry | `defineIntegration({...})` | `fromDefinitionDocument(...)`, `definitionDocumentHost(...)`, `embedDefinitionDocument(Cls, ...)` |
|
|
16
|
-
| `Definition.modules` | from `modules: { key: use(module) }` | from the document's `modules` |
|
|
17
|
-
| Webhooks | contract-2 extension, compiled to a contract-1 `Definition.extensions` binding | capability manifest `webhook` block, `Definition.webhooks` |
|
|
18
|
-
| Recurring work | `Definition.triggers` (ADR-055) | `triggersFor()` / `Definition.instanceTriggers` (ADR-055) |
|
|
19
|
-
| Handlers | functions `(payload, ctx)` | workflow steps (interpreter), `code` steps |
|
|
20
|
-
|
|
21
|
-
Neither is a serialisation of the other. They compose: `embedDefinitionDocument(DealAlerts, { definition })` gives a
|
|
22
|
-
`defineIntegration` class a document's workflows, and both kinds of trigger come out of one `triggersFor` list
|
|
23
|
-
(`tests/front-doors.test.js`).
|
|
24
|
-
|
|
25
|
-
Base branch: Stack B's tip (`claude/shared-conversation-8hgmbr-stack-9-module-methods`). Its compile target was
|
|
26
|
-
suitable as is; the only changes to Stack B code are additive (`webhookRequestFacts` exported, `static visible`
|
|
27
|
-
honoured by `integration-types`, the document-door names below as aliases).
|
|
28
|
-
|
|
29
|
-
## 2. Names
|
|
30
|
-
|
|
31
|
-
| Collision | Resolution |
|
|
32
|
-
|---|---|
|
|
33
|
-
| `defineIntegrationClass` beside `defineIntegration` | `defineIntegration` is the code front door. The document door is named for documents: `fromDefinitionDocument` (was `defineIntegrationClass`), `definitionDocumentHost` (was `defineSharedIntegrationClass`), `embedDefinitionDocument` (was `extendIntegrationClass`). The old names are aliases until Stack B merges. |
|
|
34
|
-
| "Definition" means three things | `Definition` is only the static on the class (the compile target). The ADR-063 JSON is an *integration document* (its schema becomes `integration-document.v0.1.schema.json`). `config.definition` is the installation's *document pin*. |
|
|
35
|
-
| workflow `trigger` vs ADR-055 `triggers` | One registry: `Definition.triggers`. `defineIntegration`'s `triggers` is that key (functions wrapped to get `ctx`); a contract-2 extension's `triggers` merge as `<moduleKey>.<name>` (dotted names are reserved for them). A document's `workflows[].trigger` is a workflow's start condition, not a registry; its `schedule` kind compiles into the same list through `triggersFor`. |
|
|
36
|
-
| Webhook facts in contract 2 and in capability manifests | Declared once, by the module. Verification is an Api method (`verifyWebhookSignature(input)`, with #699's input `{ headers, body, query, rawBody, method, url }`); contract 2 may name it (`verify: 'verifyWebhookSignature'`) as a manifest's `webhook.verify` does. Emitted names, account resolution and the subscription set live in the contract-2 extension; a manifest's `webhook`/`events` blocks should be generated from it (follow-up), not written beside it. `use()` options (`subscribe`, `hydrate`) reference those facts and never restate them. |
|
|
37
|
-
|
|
38
|
-
## 3. API (as built)
|
|
39
|
-
|
|
40
|
-
```text
|
|
41
|
-
defineIntegration({ name, version, display, usage, modules: { <key>: use(module, { <ext>: opts }) },
|
|
42
|
-
config, on, triggers, actions, visible, webhooks?, routes? }) // -> IntegrationBase subclass
|
|
43
|
-
defineApp({ name, integrations, ...appDefinition }) // -> { Definition }
|
|
44
|
-
use(module, extensionOptions?) // core's, not the module's
|
|
45
|
-
defineExtension(bundle) // optional; a plain object works
|
|
46
|
-
reconcileAppSubscriptions({ integrations, listInstallations?, prune?, dryRun? }) // app-scope subscriptions, add-only unless prune
|
|
47
|
-
createReconcileSubscriptionsScript({ name, extension }) // the admin script (prune, dryRun inputs)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- Handlers are `(payload, ctx)`, called without `this`. `ctx = { modules, config, log, telemetry, commands,
|
|
51
|
-
integration }`, frozen. `ctx.modules.<key>` is the installation's Api for that binding. `ctx.config` is the stored
|
|
52
|
-
config with the form defaults. `ctx.integration` is a read-only record (`id, name, status, userId, entities,
|
|
53
|
-
version`) — ADR-054 open question 6, lean taken. `commands` is created on first use.
|
|
54
|
-
- Unknown keys, an `on` key no bound extension emits, method-name handlers, bad `use()` options, trigger problems,
|
|
55
|
-
name collisions and a contract or core-range mismatch all throw when the class is defined.
|
|
56
|
-
- **Module keys cannot name an `IntegrationBase` member** (`config`, `events`, `send`, `id`, …). `_appendModules`
|
|
57
|
-
attaches each module to the instance as `this[key]`, so such a key would overwrite the member. Inside
|
|
58
|
-
`ctx.modules` any name is fine; the instance is the constraint. (ADR-054 said any identifier works.)
|
|
59
|
-
- `config` entries are JSON Schema fragments (`required: true` and `ui` are lifted) or **module fields**:
|
|
60
|
-
`module.fields.<name>(opts)` returns `{ module, schema, required, ui, options(api, { config }) }`. Fields are a
|
|
61
|
-
module export, not an extension key, because a module bound without an extension (Slack in (a)) offers them too.
|
|
62
|
-
- `actions` become `USER_ACTION` events (`DATA_ACTION`), run as `run(input, ctx)`.
|
|
63
|
-
- `visible(context)` becomes `static visible`; `integration-types` honours it when a caller passes a context.
|
|
64
|
-
|
|
65
|
-
## 4. Extension contract 2 (the stable minimum)
|
|
66
|
-
|
|
67
|
-
A contract-2 extension enabled with `use()` compiles to a contract-1 binding under `<moduleKey>-<extKey>`, so devtools
|
|
68
|
-
gives it its function (`deal-alerts__crmwebhooks`, no Prisma layer unless `useDatabase`), the router mounts its
|
|
69
|
-
routes under `/api/<integration>-integration/crm-webhooks`, and the queue worker runs its events:
|
|
70
|
-
|
|
71
|
-
1. **Receipt** (`CRM__WEBHOOKS__RECEIVED`, no database): `handshake` (answered synchronously), `verify` (401 with a
|
|
72
|
-
logged reason), `receive` splits the delivery into `{ externalId, body }` items, one `RESOLVE` message each.
|
|
73
|
-
2. **Resolve** (`CRM__WEBHOOKS__RESOLVE`, worker, dry): `resolve: { module }` looks up the installations of this
|
|
74
|
-
integration type connected to the account (the entity reverse lookup, filtered by type) and queues one
|
|
75
|
-
`DISPATCH` per installation. It fans out instead of throwing on two matches, as Stack B's app-level webhooks do.
|
|
76
|
-
3. **Dispatch** (`CRM__WEBHOOKS__DISPATCH`, worker, hydrated): `normalize(batch, { api, options, config, env, log })`
|
|
77
|
-
turns the body into `{ name, payload }` events (the HubSpot module hydrates here, with the installation's
|
|
78
|
-
credential); each runs `on['crm.<name>']`, else the extension's default handler, else it is acked and counted
|
|
79
|
-
(`frigg.extension.event_unhandled`). `CRM__WEBHOOKS__DEAL_CHANGED` etc. exist for replays.
|
|
80
|
-
4. **Subscriptions, app scope**: `reconcileAppSubscriptions` takes the union of `desired(options)` over every class
|
|
81
|
-
that binds the extension (per ENABLED installation when an option is a function of config), lists what the
|
|
82
|
-
provider has and creates what is missing. **Add-only by default**: it removes what no binding needs only with
|
|
83
|
-
`prune: true`, and only when it could read every installation's config (otherwise it reports `pruneSkipped`). A
|
|
84
|
-
dry run reports `create` and `remove` and writes nothing. **Supported setup: one provider developer app per
|
|
85
|
-
stage**, because subscriptions are app-wide; stages sharing one developer app would see, and with `prune` remove,
|
|
86
|
-
each other's. It runs at three points, with no recurring trigger:
|
|
87
|
-
- (a) **after deploy**: `frigg deploy` runs it add-only with no database (`--prune-subscriptions` prunes groups
|
|
88
|
-
whose options are all static; `--skip-subscriptions` skips it). A failure is reported, never a failed deploy.
|
|
89
|
-
- (b) **installation lifecycle**: on create and config update, an ENABLED installation whose binding options are
|
|
90
|
-
functions of its config gets the subscriptions it needs (`ensureInstallationSubscriptions`, add-only). On delete
|
|
91
|
-
nothing is called: removal needs the whole app's union, so it is a pruning reconcile's job. A failure is a
|
|
92
|
-
warning on the installation. The call sites are `onCreate`/`onUpdate`/`onDelete` overrides until the §5
|
|
93
|
-
dispatcher exists.
|
|
94
|
-
- (c) **admin script**: `createReconcileSubscriptionsScript(...)` with `prune` and `dryRun` inputs, run in the
|
|
95
|
-
stack where installations can be read. `defineApp` appends scripts that bound modules export (`module.scripts`);
|
|
96
|
-
the HubSpot module exports `hubspot.reconcile-subscriptions`.
|
|
97
|
-
5. **One bundle, both contracts**: a contract-2 bundle may keep its contract-1 face (`events` and `routes[].event`),
|
|
98
|
-
which a class binding it by hand through `Definition.extensions` (or an older core) runs. `defineIntegration`
|
|
99
|
-
ignores both. That is how the HubSpot module moves `extensions.webhooks` to contract 2 without a major version.
|
|
100
|
-
6. **Options**: `use()` options are checked against the extension's `options` schema when the class is defined;
|
|
101
|
-
function values are resolved with the installation's config and checked again.
|
|
102
|
-
|
|
103
|
-
Not in this PR (checklist on the PR): extension `lifecycle` hooks and the §5 dispatcher (the subscription call sites
|
|
104
|
-
move there), ADR-027 `env` scoping
|
|
105
|
-
in devtools and `frigg validate`, `instance`-scope subscriptions (experimental), the Slack module promotion, and
|
|
106
|
-
generating capability-manifest webhook blocks from contract 2.
|
|
107
|
-
|
|
108
|
-
## 5. Open questions
|
|
109
|
-
|
|
110
|
-
1. *(Decided: no recurring reconcile. It runs after deploy, on installation lifecycle, and from the admin script.)*
|
|
111
|
-
2. *(Decided: add-only by default, `prune` on request; one developer app per stage is the supported setup.)*
|
|
112
|
-
Alternative under evaluation: HubSpot's Webhooks Journal API makes subscriptions per portal (installation scope),
|
|
113
|
-
at the cost of a polling consumer. See the ADR-054 amendment.
|
|
114
|
-
3. **Where verification runs.** Contract 2 verifies at receipt (no database, app-scope secret from env). Stack B
|
|
115
|
-
verifies manifest webhooks in the worker (raw body carried in the message, #699). One place for both doors? Lean:
|
|
116
|
-
receipt when the secret is app-scope, the worker when it is per installation.
|
|
117
|
-
4. **Event names.** The HubSpot manifest names provider topics (`deal.propertyChange`); contract 2 emits normalized
|
|
118
|
-
names (`deal.changed`, payload `{ deal, changes }`). Lean: manifest event keys become the contract-2 names, with
|
|
119
|
-
the provider topic in `match`.
|
|
120
|
-
5. **Base trigger ctx.** `IntegrationBase#onTriggerRun` passes `{ modules: <module instances>, config, log,
|
|
121
|
-
integration: this }` to function handlers on classic classes; `defineIntegration` gives its handlers the ADR-054
|
|
122
|
-
ctx. Align the base to ADR-054 (`modules` -> Api) before Stack B merges?
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `config` (ADR-054 §4): the end user's per-install form, compiled to what `getConfigOptions()` returns today
|
|
3
|
-
* (`{ jsonSchema, uiSchema }`), so `validateConfig()`, NEEDS_CONFIG and the config/options routes work unchanged.
|
|
4
|
-
*
|
|
5
|
-
* An entry is either a JSON Schema fragment, with two conveniences:
|
|
6
|
-
*
|
|
7
|
-
* minAmount: { type: 'number', title: 'Only deals worth at least', default: 0, required: true, ui: { 'ui:help': '…' } }
|
|
8
|
-
*
|
|
9
|
-
* or a module field: what a module's `fields.<name>(opts)` returns. A field is a schema fragment plus a loader for
|
|
10
|
-
* its choices, run with the installation's own credential for that module:
|
|
11
|
-
*
|
|
12
|
-
* { module: 'slack', schema: { type: 'string', title }, required, ui, options: async (api, { config }) => [{ const, title }] }
|
|
13
|
-
*
|
|
14
|
-
* The loader runs against the binding of that module. When a module is bound twice, the author picks one with
|
|
15
|
-
* `{ ...slack.fields.channel(), from: 'chatA' }`. No field reaches a form unless the integration author put it in
|
|
16
|
-
* `config`, which keeps ADR-054 §4's rule: an extension never adds to the end user's form by itself.
|
|
17
|
-
*/
|
|
18
|
-
const { withDefaults } = require('./json-schema-lite');
|
|
19
|
-
|
|
20
|
-
const isField = (v) => Boolean(v && typeof v === 'object' && v.schema && typeof v.schema === 'object' && typeof v.options === 'function');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {object} config the `config` given to defineIntegration
|
|
24
|
-
* @param {Array<{ key, moduleName }>} bindings
|
|
25
|
-
* @param {string} where for messages
|
|
26
|
-
*/
|
|
27
|
-
function compileConfig(config = {}, bindings = [], where = 'defineIntegration') {
|
|
28
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error(`${where}: "config" must be an object keyed by field name`);
|
|
29
|
-
const properties = {}, uiSchema = {}, required = [], fields = {};
|
|
30
|
-
for (const [name, entry] of Object.entries(config)) {
|
|
31
|
-
if (!entry || typeof entry !== 'object') throw new Error(`${where}: config.${name} must be a JSON Schema fragment or a module field`);
|
|
32
|
-
if (isField(entry)) {
|
|
33
|
-
const candidates = bindings.filter((b) => (entry.from ? b.key === entry.from : b.moduleName === entry.module));
|
|
34
|
-
if (candidates.length === 0) {
|
|
35
|
-
throw new Error(`${where}: config.${name} is a ${entry.module} field${entry.from ? ` from "${entry.from}"` : ''}, but no such module is bound in "modules"`);
|
|
36
|
-
}
|
|
37
|
-
if (candidates.length > 1) {
|
|
38
|
-
throw new Error(`${where}: config.${name} is a ${entry.module} field and ${entry.module} is bound as ${candidates.map((b) => b.key).join(' and ')}; say which with { ...field, from: '<key>' }`);
|
|
39
|
-
}
|
|
40
|
-
fields[name] = { field: entry, moduleKey: candidates[0].key };
|
|
41
|
-
properties[name] = { ...entry.schema };
|
|
42
|
-
if (entry.ui) uiSchema[name] = { ...entry.ui };
|
|
43
|
-
if (entry.required) required.push(name);
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
const { required: isRequired, ui, ...schema } = entry;
|
|
47
|
-
if (isRequired !== undefined && typeof isRequired !== 'boolean') throw new Error(`${where}: config.${name}.required must be true or false`);
|
|
48
|
-
properties[name] = schema;
|
|
49
|
-
if (ui) uiSchema[name] = { ...ui };
|
|
50
|
-
if (isRequired) required.push(name);
|
|
51
|
-
}
|
|
52
|
-
const jsonSchema = { type: 'object', properties, ...(required.length ? { required } : {}) };
|
|
53
|
-
return { jsonSchema, uiSchema, fields };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The config a handler sees: the stored config with the form's defaults filled in. */
|
|
57
|
-
function resolveConfig(plan, stored = {}) {
|
|
58
|
-
return withDefaults(plan.jsonSchema, stored || {});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const asChoice = (c) => (c && typeof c === 'object'
|
|
62
|
-
? { const: c.const ?? c.value ?? c.id, title: String(c.title ?? c.label ?? c.name ?? c.const ?? c.value ?? c.id) }
|
|
63
|
-
: { const: c, title: String(c) });
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The form with each module field's choices loaded from the connected account.
|
|
67
|
-
* @param {object} plan compileConfig's result
|
|
68
|
-
* @param {(moduleKey: string) => object|undefined} apiFor the installation's Api for a binding key
|
|
69
|
-
* @param {object} values the stored config, or the form's current values on refresh
|
|
70
|
-
* @param {{ warn: Function }} [log]
|
|
71
|
-
*/
|
|
72
|
-
async function configOptions(plan, apiFor, values = {}, log) {
|
|
73
|
-
const jsonSchema = JSON.parse(JSON.stringify(plan.jsonSchema));
|
|
74
|
-
const uiSchema = JSON.parse(JSON.stringify(plan.uiSchema));
|
|
75
|
-
const config = resolveConfig(plan, values);
|
|
76
|
-
for (const [name, { field, moduleKey }] of Object.entries(plan.fields)) {
|
|
77
|
-
const api = apiFor(moduleKey);
|
|
78
|
-
let choices = [];
|
|
79
|
-
if (api) {
|
|
80
|
-
try {
|
|
81
|
-
choices = (await field.options(api, { config })) || [];
|
|
82
|
-
} catch (error) {
|
|
83
|
-
log?.warn?.('Config field options did not load', { eventName: 'frigg.define.config_options_failed', field: name, error });
|
|
84
|
-
uiSchema[name] = { ...(uiSchema[name] || {}), 'ui:help': 'The options could not be loaded from the connected account. Try again.' };
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
uiSchema[name] = { ...(uiSchema[name] || {}), 'ui:help': `Connect ${field.module} to choose.` };
|
|
88
|
-
}
|
|
89
|
-
const oneOf = choices.map(asChoice);
|
|
90
|
-
if (!oneOf.length) continue;
|
|
91
|
-
const prop = { ...field.schema, ...(field.schema.title ? { title: field.schema.title } : {}) };
|
|
92
|
-
if (prop.type === 'array') jsonSchema.properties[name] = { ...prop, uniqueItems: true, items: { ...(prop.items || { type: 'string' }), oneOf } };
|
|
93
|
-
else jsonSchema.properties[name] = { ...prop, oneOf };
|
|
94
|
-
}
|
|
95
|
-
return { jsonSchema, uiSchema };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
module.exports = { compileConfig, resolveConfig, configOptions, isField };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `defineApp` (ADR-054 §1): the app definition, as `backend/index.js` exports it today (`{ Definition }`).
|
|
3
|
-
*
|
|
4
|
-
* module.exports = defineApp({ name, integrations: [DealAlerts], database: { postgres: { enable: true } } });
|
|
5
|
-
*
|
|
6
|
-
* It checks what can be checked when the file loads: every integration is an IntegrationBase subclass with a
|
|
7
|
-
* `Definition.name`, names are unique, and the app definition passes ADR-051's `app-definition.schema.json`
|
|
8
|
-
* (through `@friggframework/schemas`, when the app has it installed; the schema treats each integration as an
|
|
9
|
-
* object, so the classes are checked here and summarised for it).
|
|
10
|
-
*
|
|
11
|
-
* It also appends the admin scripts the bound modules export (`module.scripts`, e.g. HubSpot's
|
|
12
|
-
* `hubspot.reconcile-subscriptions`) to `adminScripts`, once each by name; a script the app lists itself wins.
|
|
13
|
-
*/
|
|
14
|
-
const { IntegrationBase } = require('../integration-base');
|
|
15
|
-
const { planOf } = require('./define-integration');
|
|
16
|
-
|
|
17
|
-
function loadSchemas() {
|
|
18
|
-
try {
|
|
19
|
-
// eslint-disable-next-line global-require
|
|
20
|
-
return require('@friggframework/schemas');
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (error?.code === 'MODULE_NOT_FOUND') return null;
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function defineApp(appDefinition = {}, { schemas = loadSchemas() } = {}) {
|
|
28
|
-
const where = `defineApp(${appDefinition?.name ? `"${appDefinition.name}"` : ''})`;
|
|
29
|
-
if (!appDefinition || typeof appDefinition !== 'object') throw new Error(`${where}: expects the app definition object`);
|
|
30
|
-
const integrations = appDefinition.integrations;
|
|
31
|
-
if (!Array.isArray(integrations) || !integrations.length) throw new Error(`${where}: "integrations" must list at least one integration`);
|
|
32
|
-
const names = new Set();
|
|
33
|
-
for (const [i, Cls] of integrations.entries()) {
|
|
34
|
-
if (typeof Cls !== 'function' || !(Cls.prototype instanceof IntegrationBase)) {
|
|
35
|
-
throw new Error(`${where}: integrations[${i}] is not an integration class (defineIntegration or an IntegrationBase subclass)`);
|
|
36
|
-
}
|
|
37
|
-
const name = Cls.Definition?.name;
|
|
38
|
-
if (!name) throw new Error(`${where}: integrations[${i}] has no Definition.name`);
|
|
39
|
-
if (names.has(name)) throw new Error(`${where}: two integrations are named "${name}"`);
|
|
40
|
-
names.add(name);
|
|
41
|
-
}
|
|
42
|
-
if (schemas?.validateAppDefinition) {
|
|
43
|
-
const summary = { ...appDefinition, integrations: integrations.map((Cls) => ({ name: Cls.Definition.name })) };
|
|
44
|
-
const result = schemas.validateAppDefinition(summary);
|
|
45
|
-
if (result && result.valid === false) {
|
|
46
|
-
const errors = (result.errors || []).map((e) => (typeof e === 'string' ? e : `${e.instancePath || e.path || ''} ${e.message || ''}`.trim()));
|
|
47
|
-
throw new Error(`${where}: the app definition does not match app-definition.schema.json: ${errors.join('; ')}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const adminScripts = [...(appDefinition.adminScripts || [])];
|
|
51
|
-
const named = new Set(adminScripts.map((S) => S?.Definition?.name));
|
|
52
|
-
for (const Cls of integrations) {
|
|
53
|
-
for (const binding of planOf(Cls)?.bindings || []) {
|
|
54
|
-
const scripts = binding.module.scripts;
|
|
55
|
-
for (const Script of Array.isArray(scripts) ? scripts : Object.values(scripts || {})) {
|
|
56
|
-
const scriptName = Script?.Definition?.name;
|
|
57
|
-
if (!scriptName || named.has(scriptName)) continue;
|
|
58
|
-
named.add(scriptName);
|
|
59
|
-
adminScripts.push(Script);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return { Definition: { ...appDefinition, integrations: [...integrations], ...(adminScripts.length ? { adminScripts } : {}) } };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
module.exports = { defineApp };
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extension contract 2 (ADR-054 §2): what an API module's extension may declare beyond contract 1's routes and
|
|
3
|
-
* events. Everything is optional, and a contract-1 bundle (no `contract`, or `contract: 1`) stays valid as it is.
|
|
4
|
-
*
|
|
5
|
-
* module.exports = defineExtension({ // or a plain object with `contract: 2`: modules need no core import
|
|
6
|
-
* name: 'hubspot-webhooks',
|
|
7
|
-
* contract: 2,
|
|
8
|
-
* requires: { core: '>=2.0.0-next.0 <3' },
|
|
9
|
-
* useDatabase: false,
|
|
10
|
-
* env: ['HUBSPOT_CLIENT_SECRET'],
|
|
11
|
-
* options: { type: 'object', properties: { subscribe: {...}, hydrate: {...} } }, // schema of use(m, { <key>: opts })
|
|
12
|
-
* routes: [{ path: '/webhooks', method: 'POST' }],
|
|
13
|
-
* handshake: (input, ctx) => ({ status, headers, body }) | null, // answered at receipt, nothing queued
|
|
14
|
-
* verify: (input, ctx) => ({ valid, reason }) | boolean, // or the name of an Api method (see below)
|
|
15
|
-
* receive: (input, ctx) => [{ externalId, body }], // split one delivery, at receipt (no DB)
|
|
16
|
-
* resolve: { module: 'hubspot' }, // externalId -> installations, in the worker
|
|
17
|
-
* emits: { 'deal.changed': {}, ... }, // integration-facing names, prefixed by the module key
|
|
18
|
-
* normalize: async (batch, ctx) => [{ name: 'deal.changed', payload }], // in the worker, with the installation's api
|
|
19
|
-
* subscriptions: { scope: 'app', desired, list, create, remove, key },
|
|
20
|
-
* triggers: {}, // ADR-055 shape, merged as `<moduleKey>.<name>`
|
|
21
|
-
* events: {}, // the contract-1 face (see below)
|
|
22
|
-
* });
|
|
23
|
-
*
|
|
24
|
-
* One bundle can serve both contracts: `events` and `routes[].event` are what a class that binds the bundle by hand
|
|
25
|
-
* through `Definition.extensions` runs (and what an older core runs), and `defineIntegration` ignores both, because
|
|
26
|
-
* core receives a contract-2 route itself and never adds bare event names. A module can therefore move its
|
|
27
|
-
* extension to contract 2 without breaking the integrations that bind it the contract-1 way.
|
|
28
|
-
*
|
|
29
|
-
* `input` is the delivery as signature checks need it, the same object Stack B's runtime passes to a capability
|
|
30
|
-
* manifest's `webhook.verify` (IntegrationBase#webhookVerificationInput): `{ headers, body, query, rawBody, method, url }`.
|
|
31
|
-
* `verify` may name an Api method instead of being a function: that is how a capability manifest names it too, so a
|
|
32
|
-
* module declares how deliveries are verified once, on its Api (`verifyWebhookSignature`), for both front doors.
|
|
33
|
-
*
|
|
34
|
-
* Not in the stable 2.0.1 path (ADR-054 decisions 2026-10-03): `subscriptions.scope: 'instance'` (experimental),
|
|
35
|
-
* extension `queues`/`workers` (ADR-047), `scripts` and `lifecycle` (follow-up in this PR's checklist).
|
|
36
|
-
*/
|
|
37
|
-
const CORE_VERSION = require('../../package.json').version;
|
|
38
|
-
|
|
39
|
-
const STABLE_KEYS = new Set([
|
|
40
|
-
'name', 'contract', 'requires', 'useDatabase', 'env', 'options', 'routes', 'handshake', 'verify', 'receive',
|
|
41
|
-
'resolve', 'emits', 'normalize', 'subscriptions', 'triggers', 'events', 'description',
|
|
42
|
-
]);
|
|
43
|
-
const LATER_KEYS = new Set(['scripts', 'lifecycle', 'queues', 'workers', 'fields']);
|
|
44
|
-
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
|
|
45
|
-
|
|
46
|
-
// ---- a tiny semver range check: space-separated comparators (>=, >, <=, <, =), prereleases ordered as semver does.
|
|
47
|
-
function parseVersion(v) {
|
|
48
|
-
const m = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/.exec(String(v).trim());
|
|
49
|
-
if (!m) return null;
|
|
50
|
-
return { nums: [Number(m[1]), Number(m[2] || 0), Number(m[3] || 0)], pre: m[4] ? m[4].split('.') : [] };
|
|
51
|
-
}
|
|
52
|
-
function compareVersions(a, b) {
|
|
53
|
-
for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] < b.nums[i] ? -1 : 1;
|
|
54
|
-
if (!a.pre.length || !b.pre.length) return a.pre.length === b.pre.length ? 0 : a.pre.length ? -1 : 1;
|
|
55
|
-
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
56
|
-
const x = a.pre[i], y = b.pre[i];
|
|
57
|
-
if (x === undefined) return -1;
|
|
58
|
-
if (y === undefined) return 1;
|
|
59
|
-
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
|
60
|
-
if (nx && ny && Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
|
|
61
|
-
if (nx !== ny) return nx ? -1 : 1;
|
|
62
|
-
if (x !== y) return x < y ? -1 : 1;
|
|
63
|
-
}
|
|
64
|
-
return 0;
|
|
65
|
-
}
|
|
66
|
-
function satisfies(version, range) {
|
|
67
|
-
const v = parseVersion(version);
|
|
68
|
-
if (!v) return false;
|
|
69
|
-
return String(range).trim().split(/\s+/).every((part) => {
|
|
70
|
-
const m = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
|
|
71
|
-
const target = parseVersion(m[2]);
|
|
72
|
-
if (!target) return false;
|
|
73
|
-
const c = compareVersions(v, target);
|
|
74
|
-
switch (m[1] || '=') {
|
|
75
|
-
case '>=': return c >= 0;
|
|
76
|
-
case '>': return c > 0;
|
|
77
|
-
case '<=': return c <= 0;
|
|
78
|
-
case '<': return c < 0;
|
|
79
|
-
default: return c === 0;
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const contractOf = (ext) => (ext && ext.contract !== undefined ? ext.contract : 1);
|
|
85
|
-
const isContract2 = (ext) => contractOf(ext) === 2;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validate a contract-2 bundle. Throws with the extension name and what is wrong. Returns the bundle.
|
|
89
|
-
* @param {object} ext
|
|
90
|
-
* @param {{ coreVersion?: string, where?: string }} [opts]
|
|
91
|
-
*/
|
|
92
|
-
function validateExtension(ext, { coreVersion = CORE_VERSION, where } = {}) {
|
|
93
|
-
const name = ext?.name || '(unnamed)';
|
|
94
|
-
const fail = (msg) => { throw new Error(`${where ? `${where}: ` : ''}extension "${name}" ${msg}`); };
|
|
95
|
-
if (!ext || typeof ext !== 'object') fail('must be an object');
|
|
96
|
-
if (typeof ext.name !== 'string' || !ext.name) fail('needs a "name"');
|
|
97
|
-
const contract = contractOf(ext);
|
|
98
|
-
if (contract !== 1 && contract !== 2) fail(`declares contract ${contract}; this core supports contracts 1 and 2`);
|
|
99
|
-
if (contract === 1) return ext;
|
|
100
|
-
if (ext.requires?.core && !satisfies(coreVersion, ext.requires.core)) {
|
|
101
|
-
fail(`requires @friggframework/core ${ext.requires.core}, but this is ${coreVersion}`);
|
|
102
|
-
}
|
|
103
|
-
for (const key of Object.keys(ext)) {
|
|
104
|
-
if (key === 'fields') fail('declares "fields": config fields are a module export (module.fields), so a module bound without an extension can offer them');
|
|
105
|
-
if (LATER_KEYS.has(key)) fail(`declares "${key}", which the stable defineIntegration does not run yet (ADR-054 phasing)`);
|
|
106
|
-
if (!STABLE_KEYS.has(key)) fail(`has an unknown key "${key}"`);
|
|
107
|
-
}
|
|
108
|
-
if (ext.useDatabase !== undefined && typeof ext.useDatabase !== 'boolean') fail('"useDatabase" must be a boolean');
|
|
109
|
-
if (ext.env !== undefined && (!Array.isArray(ext.env) || ext.env.some((e) => typeof e !== 'string'))) fail('"env" must be a list of variable names');
|
|
110
|
-
if (ext.options !== undefined && (typeof ext.options !== 'object' || ext.options === null)) fail('"options" must be a JSON Schema object');
|
|
111
|
-
for (const fn of ['handshake', 'receive', 'normalize']) {
|
|
112
|
-
if (ext[fn] !== undefined && ext[fn] !== null && typeof ext[fn] !== 'function') fail(`"${fn}" must be a function`);
|
|
113
|
-
}
|
|
114
|
-
if (ext.verify !== undefined && ext.verify !== null && typeof ext.verify !== 'function' && typeof ext.verify !== 'string') {
|
|
115
|
-
fail('"verify" must be a function or the name of an Api method');
|
|
116
|
-
}
|
|
117
|
-
const routes = ext.routes || [];
|
|
118
|
-
if (!Array.isArray(routes)) fail('"routes" must be an array');
|
|
119
|
-
for (const r of routes) {
|
|
120
|
-
if (!r || typeof r.path !== 'string' || !r.path.startsWith('/')) fail('has a route without a "/path"');
|
|
121
|
-
if (typeof r.method !== 'string' || !METHODS.has(r.method.toLowerCase())) fail(`route ${r.path} has an unsupported method`);
|
|
122
|
-
// A route's `event` is the bundle's contract-1 face: a class that binds the bundle by hand through
|
|
123
|
-
// Definition.extensions (or an older core) dispatches it. defineIntegration ignores it: core receives.
|
|
124
|
-
if (r.event !== undefined && !Object.prototype.hasOwnProperty.call(ext.events || {}, r.event)) {
|
|
125
|
-
fail(`route ${r.path} names event "${r.event}", which is not in "events" (a contract-2 route needs no event: core receives it)`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (routes.length && typeof ext.receive !== 'function') fail('has routes but no "receive"');
|
|
129
|
-
if (routes.length && !ext.resolve) fail('has routes but no "resolve"');
|
|
130
|
-
if (ext.resolve && typeof ext.resolve !== 'function' && typeof ext.resolve?.module !== 'string') fail('"resolve" must be { module } or a function');
|
|
131
|
-
if (ext.emits !== undefined && (typeof ext.emits !== 'object' || Array.isArray(ext.emits))) fail('"emits" must be an object keyed by event name');
|
|
132
|
-
for (const [k, v] of Object.entries(ext.emits || {})) {
|
|
133
|
-
if (!/^[a-z][A-Za-z0-9_-]*(\.[a-z][A-Za-z0-9_-]*)*$/.test(k)) fail(`emits "${k}": names are dotted lower-camel words (deal.changed)`);
|
|
134
|
-
if (v?.handler !== undefined && typeof v.handler !== 'function') fail(`emits "${k}": "handler" must be a function`);
|
|
135
|
-
}
|
|
136
|
-
const subs = ext.subscriptions;
|
|
137
|
-
if (subs) {
|
|
138
|
-
if (subs.scope === 'instance') fail('declares instance-scope subscriptions, which are experimental (@friggframework/core/experimental)');
|
|
139
|
-
if (subs.scope !== 'app') fail('subscriptions.scope must be "app"');
|
|
140
|
-
for (const fn of ['desired', 'list', 'create']) if (typeof subs[fn] !== 'function') fail(`subscriptions.${fn} must be a function`);
|
|
141
|
-
for (const fn of ['remove', 'key']) if (subs[fn] !== undefined && typeof subs[fn] !== 'function') fail(`subscriptions.${fn} must be a function`);
|
|
142
|
-
}
|
|
143
|
-
if (ext.triggers !== undefined && (typeof ext.triggers !== 'object' || Array.isArray(ext.triggers))) fail('"triggers" must be an object');
|
|
144
|
-
return ext;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Core's helper for module authors: validates and freezes a contract-2 bundle. A plain object works the same. */
|
|
148
|
-
function defineExtension(ext) {
|
|
149
|
-
const bundle = { contract: 2, ...ext };
|
|
150
|
-
validateExtension(bundle);
|
|
151
|
-
return Object.freeze(bundle);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
module.exports = { defineExtension, validateExtension, isContract2, contractOf, satisfies, CORE_VERSION };
|