@fayz-ai/core 0.18.0 → 0.19.1
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/dist/access/engine.d.ts +15 -1
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/index.js +2 -2
- package/dist/access/index.js.map +1 -1
- package/dist/app/app-config.d.ts +25 -0
- package/dist/app/app-config.d.ts.map +1 -1
- package/dist/{chunk-ED6NBH6U.js → chunk-3TRR2YLM.js} +2 -2
- package/dist/{chunk-ED6NBH6U.js.map → chunk-3TRR2YLM.js.map} +1 -1
- package/dist/{chunk-AOIYRUTF.js → chunk-4SPEISNL.js} +19 -5
- package/dist/chunk-4SPEISNL.js.map +1 -0
- package/dist/chunk-6TVRNKWW.js +17 -0
- package/dist/chunk-6TVRNKWW.js.map +1 -0
- package/dist/{chunk-OHH26S4N.js → chunk-7M4NWLIT.js} +9 -8
- package/dist/chunk-7M4NWLIT.js.map +1 -0
- package/dist/{chunk-2VY5IJW7.js → chunk-H6XDQXN5.js} +42 -14
- package/dist/chunk-H6XDQXN5.js.map +1 -0
- package/dist/{chunk-BCI743RH.js → chunk-HVWFFFKN.js} +4 -3
- package/dist/chunk-HVWFFFKN.js.map +1 -0
- package/dist/chunk-RBPDK2WB.js +104 -0
- package/dist/chunk-RBPDK2WB.js.map +1 -0
- package/dist/{chunk-2ZAUJVYP.js → chunk-S3EEGDEK.js} +122 -48
- package/dist/chunk-S3EEGDEK.js.map +1 -0
- package/dist/{chunk-KJUD6JA5.js → chunk-ZGAGCTQM.js} +6 -5
- package/dist/chunk-ZGAGCTQM.js.map +1 -0
- package/dist/connector-runtime/index.js +1 -1
- package/dist/data/archetype.d.ts +16 -0
- package/dist/data/archetype.d.ts.map +1 -1
- package/dist/data/bulk.d.ts.map +1 -1
- package/dist/data/index.js +3 -2
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +13 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/events/log.d.ts +1 -1
- package/dist/events/log.d.ts.map +1 -1
- package/dist/fields/store.d.ts +1 -1
- package/dist/fields/store.d.ts.map +1 -1
- package/dist/i18n/index.d.ts +11 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/shell-translations.en.d.ts.map +1 -1
- package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -12
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.d.ts +8 -8
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +4 -3
- package/dist/integrations/request.d.ts +2 -2
- package/dist/integrations/request.d.ts.map +1 -1
- package/dist/integrations/runtime.d.ts +1 -1
- package/dist/integrations/runtime.d.ts.map +1 -1
- package/dist/integrations/settings.d.ts +1 -1
- package/dist/integrations/settings.d.ts.map +1 -1
- package/dist/integrations/store.d.ts +7 -7
- package/dist/integrations/store.d.ts.map +1 -1
- package/dist/interaction/haptics.d.ts +5 -0
- package/dist/interaction/haptics.d.ts.map +1 -0
- package/dist/interaction/index.d.ts +7 -0
- package/dist/interaction/index.d.ts.map +1 -0
- package/dist/interaction/index.js +3 -0
- package/dist/interaction/index.js.map +1 -0
- package/dist/interaction/keymap.d.ts +17 -0
- package/dist/interaction/keymap.d.ts.map +1 -0
- package/dist/interaction/roving.d.ts +9 -0
- package/dist/interaction/roving.d.ts.map +1 -0
- package/dist/lib/error-message.d.ts +21 -0
- package/dist/lib/error-message.d.ts.map +1 -0
- package/dist/orders/index.d.ts +114 -0
- package/dist/orders/index.d.ts.map +1 -0
- package/dist/orders/index.js +168 -0
- package/dist/orders/index.js.map +1 -0
- package/dist/plugin/index.js +2 -1
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/registry/extension-points.d.ts.map +1 -1
- package/dist/{shell-translations.pt-br-ZSSVWG6H.js → shell-translations.pt-br-GFBWBZZJ.js} +28 -12
- package/dist/shell-translations.pt-br-GFBWBZZJ.js.map +1 -0
- package/dist/testing/index.js +5 -4
- package/dist/testing.d.ts.map +1 -1
- package/dist/types/auth.d.ts +12 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/crud.d.ts +42 -0
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/onboarding.d.ts +25 -0
- package/dist/types/onboarding.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +43 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/types/setup-guide.d.ts +120 -0
- package/dist/types/setup-guide.d.ts.map +1 -0
- package/package.json +13 -4
- package/dist/chunk-2VY5IJW7.js.map +0 -1
- package/dist/chunk-2ZAUJVYP.js.map +0 -1
- package/dist/chunk-AOIYRUTF.js.map +0 -1
- package/dist/chunk-BCI743RH.js.map +0 -1
- package/dist/chunk-KJUD6JA5.js.map +0 -1
- package/dist/chunk-OHH26S4N.js.map +0 -1
- package/dist/shell-translations.pt-br-ZSSVWG6H.js.map +0 -1
package/dist/access/engine.d.ts
CHANGED
|
@@ -38,13 +38,22 @@ export declare function isOwnerProfile(profile: PermissionProfile): boolean;
|
|
|
38
38
|
* The single role-side permission check. The plan axis composes ON TOP of it in
|
|
39
39
|
* {@link resolveAccess} instead of reimplementing owner-bypass / `manage`
|
|
40
40
|
* semantics.
|
|
41
|
+
*
|
|
42
|
+
* Fails CLOSED on a missing profile. "Not hydrated yet" and "no grant" are
|
|
43
|
+
* different situations but the same answer — no — because the alternative is a
|
|
44
|
+
* frame where every financial widget paints open to someone who may not be
|
|
45
|
+
* allowed to see it. The genuinely different third case, a runtime with no RBAC
|
|
46
|
+
* at all (an SDK component under a bare host shell), is NOT representable as a
|
|
47
|
+
* null profile: it is the absence of a permissions runtime, and it is decided
|
|
48
|
+
* one level up by `usePermissionOptional` in admin/permissions/context.tsx.
|
|
41
49
|
*/
|
|
42
50
|
export declare function profileHasPermission(profile: PermissionProfile | null, feature: string, action?: string): boolean;
|
|
43
51
|
/**
|
|
44
52
|
* The org-management side of a profile: managing team, billing, settings and
|
|
45
53
|
* permissions themselves. Separate axis from `grants` — these gate the settings
|
|
46
54
|
* tabs, not a feature's rows — and it follows the same owner bypass, so the one
|
|
47
|
-
* person who can hand out access is never locked out of doing it.
|
|
55
|
+
* person who can hand out access is never locked out of doing it. Same
|
|
56
|
+
* fail-closed rule as {@link profileHasPermission} for a missing profile.
|
|
48
57
|
*/
|
|
49
58
|
export declare function profileHasSystemPermission(profile: PermissionProfile | null, permission: SystemPermission): boolean;
|
|
50
59
|
/**
|
|
@@ -77,6 +86,11 @@ export declare function effectiveProfileFor(subject: AccessSubject, unitId?: str
|
|
|
77
86
|
* previewed role). Denied → `{ allowed:false, reason:'role' }`.
|
|
78
87
|
* 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's
|
|
79
88
|
* decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.
|
|
89
|
+
*
|
|
90
|
+
* A subject whose profile has not resolved yet denies with `reason:'role'` —
|
|
91
|
+
* an unknown answer is not permission. Callers that must tell a still-loading
|
|
92
|
+
* denial apart from a final one (to paint a skeleton instead of a lock) ask the
|
|
93
|
+
* permissions runtime, not this decision: see `usePermissions().status`.
|
|
80
94
|
*/
|
|
81
95
|
export declare function resolveAccess(subject: AccessSubject, feature: string, action?: PermissionAction, options?: {
|
|
82
96
|
unitId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAe5C,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACjC,0CAA0C;IAC1C,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IACjB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAChD,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAElE;AAED
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAe5C,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACjC,0CAA0C;IAC1C,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IACjB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAChD,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAElE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAeT;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,UAAU,EAAE,gBAAgB,GAC3B,OAAO,CAIT;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,iBAAiB,GAAG,IAAI,EAC9B,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAChC,iBAAiB,GAAG,IAAI,CAmB1B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,iBAAiB,GAAG,IAAI,CAG1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,EACzB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,cAAc,CAShB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IACxG,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,OAAO,CAAA;CACnB,CAOA"}
|
package/dist/access/index.js
CHANGED
|
@@ -3,7 +3,7 @@ function isOwnerProfile(profile) {
|
|
|
3
3
|
return profile.id === "owner" || profile.name?.toLowerCase() === "owner";
|
|
4
4
|
}
|
|
5
5
|
function profileHasPermission(profile, feature, action) {
|
|
6
|
-
if (!profile) return
|
|
6
|
+
if (!profile) return false;
|
|
7
7
|
if (isOwnerProfile(profile)) return true;
|
|
8
8
|
const actions = profile.grants[feature];
|
|
9
9
|
if (!actions) return false;
|
|
@@ -11,7 +11,7 @@ function profileHasPermission(profile, feature, action) {
|
|
|
11
11
|
return actions.includes(action) || actions.includes("manage");
|
|
12
12
|
}
|
|
13
13
|
function profileHasSystemPermission(profile, permission) {
|
|
14
|
-
if (!profile) return
|
|
14
|
+
if (!profile) return false;
|
|
15
15
|
if (isOwnerProfile(profile)) return true;
|
|
16
16
|
return profile.systemPermissions?.includes(permission) ?? false;
|
|
17
17
|
}
|
package/dist/access/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/access/engine.ts","../../src/access/denial.ts","../../src/access/limits.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PermissionProfile, PermissionAction, SystemPermission } from '../types/permissions'\nimport type { Plan } from '../types/billing'\n\n// ---------------------------------------------------------------------------\n// The access engine — the ONE implementation of the role × plan × limit\n// decision, shared by every runtime that gates anything:\n//\n// - browser (saas AccessProvider / PermissionsProvider re-export from here)\n// - headless client-plane (saas access/headless.ts)\n// - the Fayz broker (server-side agent authorization imports this package)\n//\n// Hard constraint: this module (and everything under core/src/access) must stay\n// free of React, zustand and supabase — pure functions over plain data — so the\n// exact same code runs in Node/edge. Guarded by scripts/check-access-purity.mjs.\n// ---------------------------------------------------------------------------\n\n/** Why an access request was denied. `limit` only occurs on guarded writes. */\nexport type DenyReason = 'role' | 'plan' | 'limit'\n\nexport interface AccessDecision {\n allowed: boolean\n reason?: DenyReason\n}\n\n/** The minimal subject a decision needs — a projection of any richer session. */\nexport interface AccessSubject {\n /** Effective RBAC profile — the previewed role during impersonation. */\n profile: PermissionProfile | null\n /** The tenant's currently-active plan. */\n plan: Plan | null\n /**\n * Extra role a person has INSIDE a given unit, keyed by unit id.\n *\n * Additive, never subtractive: the org role applies everywhere and a unit role\n * only adds inside its unit. That choice is what makes this shippable — the\n * answer without a unit is a strict SUBSET of the answer with one, so every\n * existing call site keeps its exact meaning and none of them had to change.\n * The alternative (a unit role that overrides) can take access away by\n * accident, and every screen would have to explain which role won.\n */\n unitProfiles?: Record<string, PermissionProfile>\n /** The unit the person is looking at, used when a check does not name one. */\n activeUnitId?: string\n}\n\n/**\n * The org owner has every permission implicitly. Owner is represented by the\n * canonical role id `owner` across adapters (see org/adapters/supabase.ts\n * buildPermissionProfiles, which also grants owner all catalog perms, and the\n * mock adapter's `owner` fallback). We match by id first, falling back to the\n * (possibly localized) name for safety.\n */\nexport function isOwnerProfile(profile: PermissionProfile): boolean {\n return profile.id === 'owner' || profile.name?.toLowerCase() === 'owner'\n}\n\n/**\n * The single role-side permission check. The plan axis composes ON TOP of it in\n * {@link resolveAccess} instead of reimplementing owner-bypass / `manage`\n * semantics.\n */\nexport function profileHasPermission(\n profile: PermissionProfile | null,\n feature: string,\n action?: string,\n): boolean {\n if (!profile) return true\n\n // Owner bypass: never lock the owner out, even if the RBAC catalog is empty\n // or still loading. Because permission checks read the *effective* profile,\n // an owner previewing a non-owner role correctly loses this bypass — the\n // preview stays honest.\n if (isOwnerProfile(profile)) return true\n\n const actions = profile.grants[feature]\n if (!actions) return false\n\n if (!action) return actions.length > 0\n\n return actions.includes(action) || actions.includes('manage')\n}\n\n/**\n * The org-management side of a profile: managing team, billing, settings and\n * permissions themselves. Separate axis from `grants` — these gate the settings\n * tabs, not a feature's rows — and it follows the same owner bypass, so the one\n * person who can hand out access is never locked out of doing it.\n */\nexport function profileHasSystemPermission(\n profile: PermissionProfile | null,\n permission: SystemPermission,\n): boolean {\n if (!profile) return true\n if (isOwnerProfile(profile)) return true\n return profile.systemPermissions?.includes(permission) ?? false\n}\n\n/**\n * Does the current plan entitle `feature`? A plan gates a feature ONLY with an\n * explicit `false`; absent means \"not gated by this plan\" (allowed). This keeps\n * entitlements additive and safe: a feature the plan has never heard of stays\n * open rather than silently locking.\n */\nexport function isEntitledByPlan(plan: Plan | null, feature: string): boolean {\n return plan?.entitlements?.features?.[feature] !== false\n}\n\n/**\n * Union two profiles. Grants merge per feature, system permissions merge, and\n * the result is never smaller than either input — the additive rule, in code.\n *\n * `manage` is not expanded here: {@link profileHasPermission} already treats it\n * as satisfying any action, so expanding it would be a second implementation of\n * the same rule waiting to disagree with the first.\n */\nexport function mergeProfiles(\n base: PermissionProfile | null,\n overlay: PermissionProfile | null,\n): PermissionProfile | null {\n if (!overlay) return base\n if (!base) return overlay\n // An owner already has everything; merging onto it can only produce noise.\n if (isOwnerProfile(base)) return base\n\n const grants: Record<string, PermissionAction[]> = { ...base.grants }\n for (const [feature, actions] of Object.entries(overlay.grants ?? {})) {\n const merged = new Set([...(grants[feature] ?? []), ...actions])\n grants[feature] = [...merged]\n }\n\n const system = new Set([...(base.systemPermissions ?? []), ...(overlay.systemPermissions ?? [])])\n\n return {\n ...base,\n grants,\n systemPermissions: system.size > 0 ? [...system] : undefined,\n }\n}\n\n/**\n * The profile that applies where the person is standing: their org role, plus\n * whatever the unit adds. With no unit — or no binding in it — this is exactly\n * the org role, which is why nothing that predates units changes behaviour.\n */\nexport function effectiveProfileFor(\n subject: AccessSubject,\n unitId?: string,\n): PermissionProfile | null {\n if (!unitId) return subject.profile\n return mergeProfiles(subject.profile, subject.unitProfiles?.[unitId] ?? null)\n}\n\n/**\n * The single access decision: role FIRST, then plan.\n *\n * 1. Role — reuses `profileHasPermission` (owner bypass + `manage` semantics\n * live there, and impersonation composes for free because `profile` is the\n * previewed role). Denied → `{ allowed:false, reason:'role' }`.\n * 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's\n * decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.\n */\nexport function resolveAccess(\n subject: AccessSubject,\n feature: string,\n action?: PermissionAction,\n options?: { unitId?: string },\n): AccessDecision {\n const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId)\n if (!profileHasPermission(profile, feature, action)) {\n return { allowed: false, reason: 'role' }\n }\n if (!isEntitledByPlan(subject.plan, feature)) {\n return { allowed: false, reason: 'plan' }\n }\n return { allowed: true }\n}\n\n/**\n * Pure numeric side of a limit: given the plan cap, whether a countable\n * declaration exists, and the current usage, derive the limit math. Unlimited\n * when there's no declaration to count, no cap on the plan, or the cap is `-1`.\n */\nexport function resolveLimit(params: { cap: number | undefined; hasDeclaration: boolean; used: number }): {\n max: number\n used: number\n remaining: number\n atLimit: boolean\n unlimited: boolean\n} {\n const { cap, hasDeclaration, used } = params\n const unlimited = !hasDeclaration || cap === undefined || cap === -1\n const max = unlimited ? Infinity : cap\n const remaining = unlimited ? Infinity : Math.max(0, max - used)\n const atLimit = !unlimited && used >= max\n return { max, used, remaining, atLimit, unlimited }\n}\n","import type { DenyReason } from './engine'\n\n// ---------------------------------------------------------------------------\n// The structured denial an AGENT surface returns instead of failing silently\n// (docs/saas/ENTITLEMENTS.md § \"Superfície de agentes de IA\"). One shape for every\n// executor — FAB client-plane, the Fayz broker, and the pool RPCs (which emit\n// its jsonb mirror in their `denial` field) — so the model can explain the\n// denial and offer the upgrade path conversationally.\n// ---------------------------------------------------------------------------\n\n/** Where a denied tenant goes to upgrade — the conversational UpgradeModal. */\nexport const UPGRADE_URL = '/settings/subscription'\n\nexport interface AgentDenialLimit {\n key: string\n max: number\n used: number\n}\n\nexport interface AgentDenial {\n allowed: false\n reason: DenyReason\n /** Present when `reason === 'limit'`. */\n limit?: AgentDenialLimit\n upgradeUrl: string\n}\n\nexport type AgentGuardResult = { allowed: true } | AgentDenial\n\n/** Build a denial with the canonical upgrade URL attached. */\nexport function agentDenial(reason: DenyReason, limit?: AgentDenialLimit): AgentDenial {\n return { allowed: false, reason, ...(limit ? { limit } : {}), upgradeUrl: UPGRADE_URL }\n}\n","import type { LimitDeclaration } from '../types/entitlements'\n\n/**\n * The 4-layer limit-declaration merge, extracted so the browser AccessProvider\n * and the manifest derivation (`fayz manifest emit`) resolve the EXACT same\n * final set: core built-ins < entity-derived < plugin `declaredLimits` < app\n * overrides. Later layers win by key. Pure: pass the layers in, get the merge\n * out — deriving a declaration FROM an entity stays at the call site.\n */\nexport function mergeLimitDeclarations(\n ...layers: Array<LimitDeclaration[] | undefined>\n): LimitDeclaration[] {\n const byKey = new Map<string, LimitDeclaration>()\n for (const layer of layers) {\n for (const decl of layer ?? []) byKey.set(decl.key, decl)\n }\n return Array.from(byKey.values())\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/access/engine.ts","../../src/access/denial.ts","../../src/access/limits.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PermissionProfile, PermissionAction, SystemPermission } from '../types/permissions'\nimport type { Plan } from '../types/billing'\n\n// ---------------------------------------------------------------------------\n// The access engine — the ONE implementation of the role × plan × limit\n// decision, shared by every runtime that gates anything:\n//\n// - browser (saas AccessProvider / PermissionsProvider re-export from here)\n// - headless client-plane (saas access/headless.ts)\n// - the Fayz broker (server-side agent authorization imports this package)\n//\n// Hard constraint: this module (and everything under core/src/access) must stay\n// free of React, zustand and supabase — pure functions over plain data — so the\n// exact same code runs in Node/edge. Guarded by scripts/check-access-purity.mjs.\n// ---------------------------------------------------------------------------\n\n/** Why an access request was denied. `limit` only occurs on guarded writes. */\nexport type DenyReason = 'role' | 'plan' | 'limit'\n\nexport interface AccessDecision {\n allowed: boolean\n reason?: DenyReason\n}\n\n/** The minimal subject a decision needs — a projection of any richer session. */\nexport interface AccessSubject {\n /** Effective RBAC profile — the previewed role during impersonation. */\n profile: PermissionProfile | null\n /** The tenant's currently-active plan. */\n plan: Plan | null\n /**\n * Extra role a person has INSIDE a given unit, keyed by unit id.\n *\n * Additive, never subtractive: the org role applies everywhere and a unit role\n * only adds inside its unit. That choice is what makes this shippable — the\n * answer without a unit is a strict SUBSET of the answer with one, so every\n * existing call site keeps its exact meaning and none of them had to change.\n * The alternative (a unit role that overrides) can take access away by\n * accident, and every screen would have to explain which role won.\n */\n unitProfiles?: Record<string, PermissionProfile>\n /** The unit the person is looking at, used when a check does not name one. */\n activeUnitId?: string\n}\n\n/**\n * The org owner has every permission implicitly. Owner is represented by the\n * canonical role id `owner` across adapters (see org/adapters/supabase.ts\n * buildPermissionProfiles, which also grants owner all catalog perms, and the\n * mock adapter's `owner` fallback). We match by id first, falling back to the\n * (possibly localized) name for safety.\n */\nexport function isOwnerProfile(profile: PermissionProfile): boolean {\n return profile.id === 'owner' || profile.name?.toLowerCase() === 'owner'\n}\n\n/**\n * The single role-side permission check. The plan axis composes ON TOP of it in\n * {@link resolveAccess} instead of reimplementing owner-bypass / `manage`\n * semantics.\n *\n * Fails CLOSED on a missing profile. \"Not hydrated yet\" and \"no grant\" are\n * different situations but the same answer — no — because the alternative is a\n * frame where every financial widget paints open to someone who may not be\n * allowed to see it. The genuinely different third case, a runtime with no RBAC\n * at all (an SDK component under a bare host shell), is NOT representable as a\n * null profile: it is the absence of a permissions runtime, and it is decided\n * one level up by `usePermissionOptional` in admin/permissions/context.tsx.\n */\nexport function profileHasPermission(\n profile: PermissionProfile | null,\n feature: string,\n action?: string,\n): boolean {\n if (!profile) return false\n\n // Owner bypass: never lock the owner out, even if the RBAC catalog is empty\n // or still loading. Because permission checks read the *effective* profile,\n // an owner previewing a non-owner role correctly loses this bypass — the\n // preview stays honest.\n if (isOwnerProfile(profile)) return true\n\n const actions = profile.grants[feature]\n if (!actions) return false\n\n if (!action) return actions.length > 0\n\n return actions.includes(action) || actions.includes('manage')\n}\n\n/**\n * The org-management side of a profile: managing team, billing, settings and\n * permissions themselves. Separate axis from `grants` — these gate the settings\n * tabs, not a feature's rows — and it follows the same owner bypass, so the one\n * person who can hand out access is never locked out of doing it. Same\n * fail-closed rule as {@link profileHasPermission} for a missing profile.\n */\nexport function profileHasSystemPermission(\n profile: PermissionProfile | null,\n permission: SystemPermission,\n): boolean {\n if (!profile) return false\n if (isOwnerProfile(profile)) return true\n return profile.systemPermissions?.includes(permission) ?? false\n}\n\n/**\n * Does the current plan entitle `feature`? A plan gates a feature ONLY with an\n * explicit `false`; absent means \"not gated by this plan\" (allowed). This keeps\n * entitlements additive and safe: a feature the plan has never heard of stays\n * open rather than silently locking.\n */\nexport function isEntitledByPlan(plan: Plan | null, feature: string): boolean {\n return plan?.entitlements?.features?.[feature] !== false\n}\n\n/**\n * Union two profiles. Grants merge per feature, system permissions merge, and\n * the result is never smaller than either input — the additive rule, in code.\n *\n * `manage` is not expanded here: {@link profileHasPermission} already treats it\n * as satisfying any action, so expanding it would be a second implementation of\n * the same rule waiting to disagree with the first.\n */\nexport function mergeProfiles(\n base: PermissionProfile | null,\n overlay: PermissionProfile | null,\n): PermissionProfile | null {\n if (!overlay) return base\n if (!base) return overlay\n // An owner already has everything; merging onto it can only produce noise.\n if (isOwnerProfile(base)) return base\n\n const grants: Record<string, PermissionAction[]> = { ...base.grants }\n for (const [feature, actions] of Object.entries(overlay.grants ?? {})) {\n const merged = new Set([...(grants[feature] ?? []), ...actions])\n grants[feature] = [...merged]\n }\n\n const system = new Set([...(base.systemPermissions ?? []), ...(overlay.systemPermissions ?? [])])\n\n return {\n ...base,\n grants,\n systemPermissions: system.size > 0 ? [...system] : undefined,\n }\n}\n\n/**\n * The profile that applies where the person is standing: their org role, plus\n * whatever the unit adds. With no unit — or no binding in it — this is exactly\n * the org role, which is why nothing that predates units changes behaviour.\n */\nexport function effectiveProfileFor(\n subject: AccessSubject,\n unitId?: string,\n): PermissionProfile | null {\n if (!unitId) return subject.profile\n return mergeProfiles(subject.profile, subject.unitProfiles?.[unitId] ?? null)\n}\n\n/**\n * The single access decision: role FIRST, then plan.\n *\n * 1. Role — reuses `profileHasPermission` (owner bypass + `manage` semantics\n * live there, and impersonation composes for free because `profile` is the\n * previewed role). Denied → `{ allowed:false, reason:'role' }`.\n * 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's\n * decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.\n *\n * A subject whose profile has not resolved yet denies with `reason:'role'` —\n * an unknown answer is not permission. Callers that must tell a still-loading\n * denial apart from a final one (to paint a skeleton instead of a lock) ask the\n * permissions runtime, not this decision: see `usePermissions().status`.\n */\nexport function resolveAccess(\n subject: AccessSubject,\n feature: string,\n action?: PermissionAction,\n options?: { unitId?: string },\n): AccessDecision {\n const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId)\n if (!profileHasPermission(profile, feature, action)) {\n return { allowed: false, reason: 'role' }\n }\n if (!isEntitledByPlan(subject.plan, feature)) {\n return { allowed: false, reason: 'plan' }\n }\n return { allowed: true }\n}\n\n/**\n * Pure numeric side of a limit: given the plan cap, whether a countable\n * declaration exists, and the current usage, derive the limit math. Unlimited\n * when there's no declaration to count, no cap on the plan, or the cap is `-1`.\n */\nexport function resolveLimit(params: { cap: number | undefined; hasDeclaration: boolean; used: number }): {\n max: number\n used: number\n remaining: number\n atLimit: boolean\n unlimited: boolean\n} {\n const { cap, hasDeclaration, used } = params\n const unlimited = !hasDeclaration || cap === undefined || cap === -1\n const max = unlimited ? Infinity : cap\n const remaining = unlimited ? Infinity : Math.max(0, max - used)\n const atLimit = !unlimited && used >= max\n return { max, used, remaining, atLimit, unlimited }\n}\n","import type { DenyReason } from './engine'\n\n// ---------------------------------------------------------------------------\n// The structured denial an AGENT surface returns instead of failing silently\n// (docs/saas/ENTITLEMENTS.md § \"Superfície de agentes de IA\"). One shape for every\n// executor — FAB client-plane, the Fayz broker, and the pool RPCs (which emit\n// its jsonb mirror in their `denial` field) — so the model can explain the\n// denial and offer the upgrade path conversationally.\n// ---------------------------------------------------------------------------\n\n/** Where a denied tenant goes to upgrade — the conversational UpgradeModal. */\nexport const UPGRADE_URL = '/settings/subscription'\n\nexport interface AgentDenialLimit {\n key: string\n max: number\n used: number\n}\n\nexport interface AgentDenial {\n allowed: false\n reason: DenyReason\n /** Present when `reason === 'limit'`. */\n limit?: AgentDenialLimit\n upgradeUrl: string\n}\n\nexport type AgentGuardResult = { allowed: true } | AgentDenial\n\n/** Build a denial with the canonical upgrade URL attached. */\nexport function agentDenial(reason: DenyReason, limit?: AgentDenialLimit): AgentDenial {\n return { allowed: false, reason, ...(limit ? { limit } : {}), upgradeUrl: UPGRADE_URL }\n}\n","import type { LimitDeclaration } from '../types/entitlements'\n\n/**\n * The 4-layer limit-declaration merge, extracted so the browser AccessProvider\n * and the manifest derivation (`fayz manifest emit`) resolve the EXACT same\n * final set: core built-ins < entity-derived < plugin `declaredLimits` < app\n * overrides. Later layers win by key. Pure: pass the layers in, get the merge\n * out — deriving a declaration FROM an entity stays at the call site.\n */\nexport function mergeLimitDeclarations(\n ...layers: Array<LimitDeclaration[] | undefined>\n): LimitDeclaration[] {\n const byKey = new Map<string, LimitDeclaration>()\n for (const layer of layers) {\n for (const decl of layer ?? []) byKey.set(decl.key, decl)\n }\n return Array.from(byKey.values())\n}\n"]}
|
package/dist/app/app-config.d.ts
CHANGED
|
@@ -11,6 +11,30 @@ export interface AppConfigPlugin {
|
|
|
11
11
|
/** When a trial or courtesy ends; null is forever. */
|
|
12
12
|
expires_at: string | null;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* One application this tenant runs (migration 001).
|
|
16
|
+
*
|
|
17
|
+
* An app is a LENS, not a boundary: two apps on one tenant read the same
|
|
18
|
+
* `public.products`. `url` is both where the switcher sends you and the
|
|
19
|
+
* allow-list the session handoff checks against.
|
|
20
|
+
*/
|
|
21
|
+
export interface AppConfigApp {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
/** Lucide icon NAME, resolved through the UI's curated registry. */
|
|
25
|
+
icon: string | null;
|
|
26
|
+
accent_color: string | null;
|
|
27
|
+
url: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Which account to land in when this app is opened (008).
|
|
30
|
+
*
|
|
31
|
+
* The active tenant when it runs the app — staying put is the common case —
|
|
32
|
+
* otherwise the first of the caller's accounts that does. Absent on a pool
|
|
33
|
+
* that has not run 008, where every listed app belongs to the active tenant
|
|
34
|
+
* by construction.
|
|
35
|
+
*/
|
|
36
|
+
tenant_id?: string | null;
|
|
37
|
+
}
|
|
14
38
|
/** The tenant's difference over the menu the plugins offer. */
|
|
15
39
|
export interface AppConfigNavEntry {
|
|
16
40
|
key: string;
|
|
@@ -24,6 +48,7 @@ export interface AppConfig {
|
|
|
24
48
|
branding?: Record<string, unknown>;
|
|
25
49
|
settings?: Record<string, unknown>;
|
|
26
50
|
plugins: AppConfigPlugin[];
|
|
51
|
+
apps: AppConfigApp[];
|
|
27
52
|
nav: AppConfigNavEntry[];
|
|
28
53
|
permissions?: string[];
|
|
29
54
|
generated_at?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/app/app-config.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,+DAA+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC7B,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,sDAAsD;IACtD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI,CAAA;IACjD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,GAAG,EAAE,iBAAiB,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/app/app-config.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,+DAA+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC7B,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,sDAAsD;IACtD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI,CAAA;IACjD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,IAAI,EAAE,YAAY,EAAE,CAAA;IACpB,GAAG,EAAE,iBAAiB,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,EAAE,SAMhB,CAAA;AAcf;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GACnD,OAAO,CAAC,SAAS,CAAC,CAcpB;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAG3D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,CAcrE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,mBAAmB,EAAE,CA2B/E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAChH,OAAO,EAAE,CAAC,EAAE,EACZ,GAAG,EAAE,iBAAiB,EAAE,GACvB,CAAC,EAAE,CAeL;AAQD,6EAA6E;AAC7E,eAAO,MAAM,0BAA0B,0CAA2C,CAAA;AAElF,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC3H;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,YAAY,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,GAAG,CAAC,CAoClG"}
|
|
@@ -344,5 +344,5 @@ function reconcileMirror(input) {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
export { CONNECTOR_REDEMPTION_ENV_KEYS, CONNECTOR_RUNTIME_TOKEN_HEADER, ConnectorCredentialUnavailableError, ConnectorHttpError, RateLimitExceededError, advanceWindow, backoffDelay, classify, createRateLimiter, cursorCompleteness, fetchWithRetry, isExpiring, paginate, parseRetryAfter, reconcileMirror, redeemConnectorCredential, redemptionFailure, redemptionHost, serializeCursor, sliceWindow, statusNeedsBody };
|
|
347
|
-
//# sourceMappingURL=chunk-
|
|
348
|
-
//# sourceMappingURL=chunk-
|
|
347
|
+
//# sourceMappingURL=chunk-3TRR2YLM.js.map
|
|
348
|
+
//# sourceMappingURL=chunk-3TRR2YLM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/integrations/runtime.ts"],"names":[],"mappings":"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","file":"chunk-ED6NBH6U.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Connector runtime — the mechanics every connector's data plane repeats.\n// ---------------------------------------------------------------------------\n// Lifted out of plugins/plugin-agenda/functions/_shared/google-calendar-runtime.js\n// (FAY-1376): that module was far more generic than its name admitted. Retry,\n// backoff, Retry-After parsing, expiry-with-lead and pagination are HTTP\n// mechanics, not Google's. Only `listAllEventChanges` was genuinely Google's,\n// and it stays there — rewritten on top of `paginate`.\n//\n// This module has NO runtime imports, on purpose. It is built to its own entry\n// (`@fayz-ai/core/connector-runtime`) so a Supabase Edge Function can load it\n// under Deno without dragging React, supabase-js or the plugin registry along.\n// Everything here is pure and transport-injected: `fetch`, the clock and the\n// sleep are parameters, so every decision below is provable under node:test.\n\n// ---------------------------------------------------------------------------\n// Resuming between runs — three things, not one\n// ---------------------------------------------------------------------------\n// A single `cursor: string` collapses a distinction that decides whether data\n// is lost. What separates these is not the field's name, it is whether ABSENCE\n// carries meaning:\n//\n// delta — the SERVER tracks what changed and reports tombstones (Google's\n// syncToken). A linked record that stops appearing was deleted.\n// window — the CLIENT computes a date range; the server has no memory (Bling).\n// A record that stops appearing merely left the window.\n// period — a closed, often sealed statement period (PlugBank). Deletion does\n// not apply.\n//\n// Flatten them and the engine cancels local records that were only out of range.\nexport type Cursor =\n | { kind: 'delta'; token: string | null; restartFrom?: string }\n | { kind: 'window'; from: string; to: string; overlapMs: number }\n | { kind: 'period'; period: string; sealed: boolean }\n\n/** `authoritative` = absence means deleted. `additive` = absence means nothing. */\nexport type Completeness = 'authoritative' | 'additive'\n\n/**\n * What a cursor kind can promise about the records it did NOT return.\n *\n * Only a delta cursor can promise anything: the server counted the changes and\n * named the tombstones. A window is computed by the client against a server\n * with no memory, so the records outside it are not absent — they were never\n * asked for.\n */\nexport function cursorCompleteness(cursor: Cursor): Completeness {\n return cursor.kind === 'delta' ? 'authoritative' : 'additive'\n}\n\n// ---------------------------------------------------------------------------\n// Reading the credential back — the other half of the sink\n// ---------------------------------------------------------------------------\n// The browser writes a tenant's credential to the platform and gets a handle\n// (see `ConnectorCredentialSink` in @fayz-ai/sdk). Something has to read it at\n// run time, and until now nothing did: `POST …/connections/redeem` shipped with\n// no caller at all, so every connector still had to find its provider key\n// somewhere else — which is how three of them ended up with a plaintext column\n// in a pool database.\n//\n// It lives HERE, in the one module already written to be loaded by a Supabase\n// Edge Function under Deno, and not in a per-plugin `_shared/` copy. FAY-1390\n// is about the copies that already exist; adding a third alongside them would\n// be adding the problem to the fix.\n//\n// Redemption takes TWO keys and neither is sufficient alone:\n//\n// the data-plane secret proves the caller is not a browser\n// the runtime-data token proves WHICH TENANT it is acting for\n//\n// The tenant therefore comes off verified claims and is not nameable in the\n// request — a caller holding the secret still cannot reach another tenant's\n// credential, which is the FAY-1375 bug class closed by construction. The\n// consequence for a caller: the runtime token arrives WITH the request being\n// served; an edge function cannot mint one, and must not try.\n\n/**\n * The header a runtime-data token rides in, on its way to a data plane.\n *\n * It lives beside `redeemConnectorCredential` because the two only make sense\n * together: the function cannot mint a token and must forward the one the\n * request carried, so the caller and the callee have to agree on a spelling.\n * Two connectors that each picked their own would fail at exactly the moment a\n * merchant clicks Sincronizar, and would look like a credential problem.\n *\n * Lower-case: an edge function reads `request.headers.get(...)`, which is\n * case-insensitive, but a CORS `Access-Control-Allow-Headers` list is compared\n * against the lower-cased name the browser sends.\n */\nexport const CONNECTOR_RUNTIME_TOKEN_HEADER = 'x-fayz-runtime-token'\n\n/** What redemption returns. `secrets` is the credential — it goes to the provider and nowhere else. */\nexport interface RedeemedConnectorCredential {\n connectionId: string\n connectorSlug: string\n tenantKey: string\n authKind: string\n /**\n * For `api-key` and `mtls`, the fields the merchant typed. For `oauth`, a\n * freshly minted access token — `refresh_token` is removed on the platform\n * side, so the field that could mint further tokens has no route out.\n */\n secrets: Record<string, string>\n /** Present for `oauth`: when the returned access token stops working. */\n expiresAt?: string\n}\n\n/** The credential could not be read. Never carries the secret, the token or the body. */\nexport class ConnectorCredentialUnavailableError extends Error {\n readonly status: number | null\n constructor(message: string, status: number | null = null) {\n super(message)\n this.name = 'ConnectorCredentialUnavailableError'\n this.status = status\n }\n}\n\nexport interface RedeemConnectorCredentialOptions {\n /** Which connector's credential, e.g. 'appmax'. */\n connectorSlug: string\n /**\n * The runtime-data token that came in with the request being served.\n *\n * It is what says which tenant this run is for, and the function has no way\n * to mint one — so it is required for anything serving a signed-in operator.\n * The one alternative is {@link tenantKey}; exactly one of the two must be set.\n */\n runtimeToken?: string\n /**\n * MACHINE mode: name the tenant instead of proving it with a token.\n *\n * For callers a session never reaches — a shopper with no account, a cron job\n * with no user. The redemption secret alone then stands for \"not a browser\",\n * so the platform bounds this path to `MACHINE_REDEEMABLE_CONNECTORS` and\n * audits every use as `machine`.\n *\n * Consequence worth stating plainly: whoever holds the secret can reach any\n * tenant of that project through this door. That is why it only ever lives in\n * an edge function's environment, never in anything a browser loads.\n */\n tenantKey?: string\n /** Defaults to `FAYZ_API_URL` from the environment. */\n apiUrl?: string\n /** Defaults to `FAYZ_PROJECT_ID`. */\n projectId?: string\n /** Defaults to `CONNECTOR_REDEMPTION_SECRET`. */\n redemptionSecret?: string\n /** Defaults to the ambient `fetch`. Injected so the call is provable under node:test. */\n fetchImpl?: (input: string, init: Record<string, unknown>) => Promise<{\n ok: boolean\n status: number\n text(): Promise<string>\n }>\n /** Defaults to `Deno.env.get`, falling back to `process.env`. */\n env?: (name: string) => string | undefined\n}\n\n/**\n * The header a data-plane call carries its runtime token in.\n *\n * A header and not the body: `Authorization` already belongs to the platform\n * the function is deployed on (Supabase checks the caller's JWT there), and a\n * token in a body is a token in whatever logs that body.\n *\n * It is declared ONCE, above, in the module that loads under Deno, because both\n * halves have to name it and only one of them can import the other's: the\n * browser side gets it through `@fayz-ai/core` and the function side through\n * `@fayz-ai/core/connector-runtime`. Written down once, or the second caller\n * spells it differently and neither is wrong at review time — which is exactly\n * what two branches did here, each adding its own `const` with the same value.\n */\n\n/** The three names `provisionConnectorRuntimeEnv` puts into a project's functions. */\nexport const CONNECTOR_REDEMPTION_ENV_KEYS = [\n 'FAYZ_API_URL',\n 'FAYZ_PROJECT_ID',\n 'CONNECTOR_REDEMPTION_SECRET',\n] as const\n\nfunction ambientEnv(name: string): string | undefined {\n const scope = globalThis as {\n Deno?: { env?: { get(key: string): string | undefined } }\n process?: { env?: Record<string, string | undefined> }\n }\n try {\n return scope.Deno?.env?.get(name) ?? scope.process?.env?.[name] ?? undefined\n } catch {\n // Deno throws rather than returning undefined when the permission is absent.\n return undefined\n }\n}\n\n/**\n * `FAYZ_API_URL` is provisioned as `https://host/api` — already one segment in.\n * Accept both spellings so a hand-set variable without it cannot 404 at the one\n * moment a sync needs its key.\n */\nfunction redemptionApiRoot(value: string): string {\n const trimmed = value.trim().replace(/\\/+$/, '')\n return /\\/api$/.test(trimmed) ? trimmed : `${trimmed}/api`\n}\n\n/** Whether something other than the API answered — a tunnel page, a proxy, a login. */\nfunction looksLikeHtml(body: string): boolean {\n return /^\\s*(<!doctype html|<html)/i.test(body ?? '')\n}\n\n/**\n * The platform's own sentence — and ONLY the platform's.\n *\n * A failure body is quoted here, which is a rule the rest of this module spends\n * paragraphs refusing to break: the run row this ends up on is rendered in the\n * merchant's UI, and an error message is the easiest place for a secret to end\n * up. So the quote is bounded to the envelope the Fayz API itself returns\n * (`{ status: 'error', message }`) — a provider's body, a proxy page or any\n * other shape is never repeated, only classified.\n *\n * That narrowness is what the diagnosis needed anyway: \"Runtime data token is\n * for project X, but this call asks about Y\" is our own sentence, and it is the\n * one that turned an unreadable 403 into a one-line fix.\n */\nfunction readRefusal(body: string): string {\n const text = (body ?? '').trim()\n if (!text || !text.startsWith('{')) return ''\n try {\n const parsed = JSON.parse(text) as { status?: unknown; message?: unknown }\n if (parsed.status !== 'error' || typeof parsed.message !== 'string') return ''\n return parsed.message.slice(0, 300)\n } catch {\n return ''\n }\n}\n\n/** The host a redemption was addressed to. Never the path — it carries the ids. */\nexport function redemptionHost(url: string): string {\n try {\n return new URL(url).host\n } catch {\n return 'the platform'\n }\n}\n\n/**\n * What a failed redemption should say. Exported so it can be graded — the value\n * is entirely in what it names.\n *\n * `door` is `'session'` for an operator's token, or the tenant key a machine\n * call named. Both are safe to print; neither the secret nor the token is.\n */\nexport function redemptionFailure(\n connectorSlug: string,\n status: number,\n url: string,\n door: 'session' | string,\n said = '',\n): string {\n const where = redemptionHost(url)\n const who = door === 'session' ? 'a sessão do operador' : `tenantKey ${door}`\n // An HTML body means something ANSWERED FOR the platform — a dead tunnel, a\n // proxy, a login page. Reading that as \"no credential\" sends someone auditing\n // a connection that was never asked about.\n if (looksLikeHtml(said)) {\n return (\n `The platform did not answer at ${where} — a proxy or tunnel page came back instead of the ` +\n `${connectorSlug} credential (${status}). Check FAYZ_API_URL on this project's functions: it ` +\n `points somewhere that is not serving the Fayz API right now`\n )\n }\n const detail = readRefusal(said)\n const base =\n `The platform did not return the ${connectorSlug} credential (${status}) — ${where}, ${who}` +\n (detail ? `: ${detail}` : '')\n if (status !== 404) return base\n // The one status that reads as a bug in the connector and almost never is.\n return (\n `${base}. 404 means that platform has no connected ${connectorSlug} credential for that tenant — ` +\n `most often the connection was authorised against a different environment, or the tenant proving ` +\n `this call is not the one that authorised`\n )\n}\n\n/**\n * Read a tenant's connector credential from the platform, server-side.\n *\n * Everything that could leak is deliberately absent from what this can throw:\n * the response body is never quoted, because the only route that returns a\n * credential is this one and an error message is the easiest place for it to\n * end up.\n */\nexport async function redeemConnectorCredential(\n options: RedeemConnectorCredentialOptions,\n): Promise<RedeemedConnectorCredential> {\n const env = options.env ?? ambientEnv\n const connectorSlug = (options.connectorSlug ?? '').trim()\n const runtimeToken = (options.runtimeToken ?? '').trim()\n const apiUrl = (options.apiUrl ?? env('FAYZ_API_URL') ?? '').trim()\n const projectId = (options.projectId ?? env('FAYZ_PROJECT_ID') ?? '').trim()\n const secret = (options.redemptionSecret ?? env('CONNECTOR_REDEMPTION_SECRET') ?? '').trim()\n\n if (!connectorSlug) throw new ConnectorCredentialUnavailableError('connectorSlug is required to redeem a credential')\n // Unconfigured is a refusal, in every environment. A data plane that answers\n // without its secret is one that was never wired, reporting success.\n if (!apiUrl || !projectId || !secret) {\n throw new ConnectorCredentialUnavailableError(\n `Connector redemption is not configured — expected ${CONNECTOR_REDEMPTION_ENV_KEYS.join(', ')} in the environment`,\n )\n }\n const tenantKey = (options.tenantKey ?? '').trim()\n if (!runtimeToken && !tenantKey) {\n throw new ConnectorCredentialUnavailableError(\n 'No runtime token and no tenantKey on this call, so there is no tenant to redeem a credential for',\n )\n }\n\n const fetchImpl =\n options.fetchImpl ??\n ((input: string, init: Record<string, unknown>) =>\n (globalThis as { fetch: (i: string, n: unknown) => Promise<any> }).fetch(input, init))\n\n const url =\n `${redemptionApiRoot(apiUrl)}/public/projects/${encodeURIComponent(projectId)}` +\n `/connectors/connections/redeem?connectorSlug=${encodeURIComponent(connectorSlug)}`\n\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n // Two keys: the secret says \"not a browser\", the token says \"this tenant\".\n 'x-fayz-connector-secret': secret,\n ...(runtimeToken ? { Authorization: `Bearer ${runtimeToken}` } : {}),\n ...(runtimeToken ? {} : { 'Content-Type': 'application/json' }),\n },\n // Machine mode: no Authorization, tenant named in the body. The platform\n // bounds it to MACHINE_REDEEMABLE_CONNECTORS and audits every use.\n ...(runtimeToken ? {} : { body: JSON.stringify({ tenantKey }) }),\n })\n\n if (!response.ok) {\n // WHICH platform, through WHICH door, and what it actually said. Quoting the\n // body is safe by construction here: the credential only ever comes back on\n // a 200, so a failure body is an error and never a secret.\n const said = await response.text().catch(() => '')\n throw new ConnectorCredentialUnavailableError(\n redemptionFailure(connectorSlug, response.status, url, runtimeToken ? 'session' : tenantKey, said),\n response.status,\n )\n }\n\n let parsed: Partial<RedeemedConnectorCredential> | undefined\n try {\n parsed = JSON.parse(await response.text()) as Partial<RedeemedConnectorCredential>\n } catch {\n throw new ConnectorCredentialUnavailableError(\n `The platform answered ${response.status} with something that is not a credential`,\n response.status,\n )\n }\n\n const secrets = parsed?.secrets\n if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {\n throw new ConnectorCredentialUnavailableError(\n `The ${connectorSlug} connection carries no credential`,\n response.status,\n )\n }\n\n return {\n connectionId: String(parsed?.connectionId ?? ''),\n connectorSlug: String(parsed?.connectorSlug ?? connectorSlug),\n tenantKey: String(parsed?.tenantKey ?? ''),\n authKind: String(parsed?.authKind ?? 'api-key'),\n secrets: secrets as Record<string, string>,\n ...(typeof parsed?.expiresAt === 'string' ? { expiresAt: parsed.expiresAt } : {}),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * A provider answered with a status the caller could not use. `body` is carried\n * because some providers put the real reason there and only there — Google\n * signals quota exhaustion as 403 with `rateLimitExceeded` in the payload, and a\n * classifier that only sees the status cannot tell it from a permission denial.\n */\nexport class ConnectorHttpError extends Error {\n readonly status: number\n readonly retryAfterMs: number | null\n readonly body: string | null\n readonly details: unknown\n\n constructor(\n status: number,\n message: string,\n options: { retryAfterMs?: number | null; body?: string | null; details?: unknown } = {},\n ) {\n super(message)\n this.name = 'ConnectorHttpError'\n this.status = status\n this.retryAfterMs = options.retryAfterMs ?? null\n this.body = options.body ?? null\n this.details = options.details ?? null\n }\n}\n\n// ---------------------------------------------------------------------------\n// Retry-After and backoff\n// ---------------------------------------------------------------------------\n\n/**\n * RFC 7231 `Retry-After`: either delta-seconds or an HTTP-date. Returns\n * milliseconds, or null when the header is absent or unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined, now: number = Date.now()): number | null {\n if (!value) return null\n const seconds = Number(value)\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1_000)\n const at = Date.parse(value)\n return Number.isFinite(at) ? Math.max(0, at - now) : null\n}\n\nexport interface BackoffOptions {\n baseDelayMs?: number\n maxDelayMs?: number\n random?: () => number\n}\n\n/** Exponential backoff with ±25% jitter, capped. Attempt is 1-based. */\nexport function backoffDelay(attempt: number, options: BackoffOptions = {}): number {\n const baseDelayMs = options.baseDelayMs ?? 250\n const maxDelayMs = options.maxDelayMs ?? 5_000\n const random = options.random ?? Math.random\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1))\n return Math.round(exponential * (0.75 + random() * 0.5))\n}\n\n// ---------------------------------------------------------------------------\n// The verdict — retry is not a boolean\n// ---------------------------------------------------------------------------\n// `isRetryable(status)` cannot express that 401 means retry AFTER RE-AUTHENTICATING,\n// 429 means retry AFTER SLEEPING, and Google's 410 means retry AFTER DISCARDING\n// THE CURSOR. Three different recoveries, which is why they lived in three\n// unrelated places and why the 403 case was simply missing.\n//\n// `classify` owns `delayMs` as well as the action. That is what lets one\n// connector honour `Retry-After` while another reads `X-RateLimit-Reset`,\n// without either of them re-implementing the loop.\n\nexport type Verdict =\n /** Hand the response back to the caller — success, or a client error it must see. */\n | { action: 'return' }\n /** Wait, then repeat the same request. */\n | { action: 'retry'; delayMs: number }\n /** The credential is stale: renew it, then repeat the request once. */\n | { action: 'reauth' }\n /** The cursor is gone: discard it and restart the run from scratch. */\n | { action: 'reset-cursor' }\n /** Nothing left to try. */\n | { action: 'fail'; reason: string }\n\n/** What `classify` is shown about one attempt. */\nexport interface AttemptOutcome {\n /** null when the transport itself failed and there is no response. */\n status: number | null\n headers?: { get(name: string): string | null } | null\n /** Response body, when the caller peeked at it. See {@link ClassifyOptions.needsBody}. */\n body?: string | null\n error?: unknown\n /** 1-based. */\n attempt: number\n maxAttempts: number\n}\n\nexport interface ClassifyOptions extends BackoffOptions {\n now?: () => number\n /** Statuses to retry beyond the defaults (408, 425, 429, 5xx). */\n retryableStatuses?: number[]\n /** Statuses meaning \"the cursor is stale, start over\". Google: 410. */\n resetCursorStatuses?: number[]\n /** Statuses meaning \"renew the credential and repeat\". Default: 401. */\n reauthStatuses?: number[]\n /** Where this provider states its wait. Default: `Retry-After` (RFC 7231). */\n readRetryAfter?(headers: { get(name: string): string | null } | null | undefined, now: number): number | null\n /**\n * Whether a 403 is quota exhaustion rather than a permission denial.\n *\n * This is the defect the extraction fixes. `isRetryableGoogleStatus` returned\n * false for 403 — and 403 with `rateLimitExceeded` in the body is exactly how\n * the Google Calendar API reports the quota being blown, the single most\n * characteristic failure of the provider it was written for.\n */\n isQuotaBody?(body: string | null | undefined, status: number): boolean\n /** Statuses whose body `classify` needs. Default: [403], for the case above. */\n needsBody?: number[]\n}\n\nconst DEFAULT_RETRYABLE = new Set([408, 425, 429])\n\n/** Statuses whose body must be read before `classify` can decide. */\nexport function statusNeedsBody(status: number | null, options: ClassifyOptions = {}): boolean {\n if (status === null) return false\n return (options.needsBody ?? [403]).includes(status)\n}\n\n/** The one retry policy. See {@link Verdict}. */\nexport function classify(outcome: AttemptOutcome, options: ClassifyOptions = {}): Verdict {\n const { status, attempt, maxAttempts } = outcome\n const exhausted = attempt >= maxAttempts\n const now = options.now?.() ?? Date.now()\n const readRetryAfter = options.readRetryAfter ?? ((h, at) => parseRetryAfter(h?.get('retry-after'), at))\n const wait = () => readRetryAfter(outcome.headers, now) ?? backoffDelay(attempt, options)\n\n // No response at all. Retrying is the only recovery, and once the attempts are\n // spent there is nothing to hand back — unlike a status, which the caller can\n // still read.\n if (status === null) {\n if (exhausted) return { action: 'fail', reason: String((outcome.error as Error)?.message ?? outcome.error ?? 'request failed') }\n return { action: 'retry', delayMs: backoffDelay(attempt, options) }\n }\n\n if (status < 400) return { action: 'return' }\n\n if ((options.reauthStatuses ?? [401]).includes(status)) {\n return exhausted ? { action: 'return' } : { action: 'reauth' }\n }\n\n if ((options.resetCursorStatuses ?? []).includes(status)) return { action: 'reset-cursor' }\n\n const retryable =\n DEFAULT_RETRYABLE.has(status) ||\n (status >= 500 && status <= 599) ||\n (options.retryableStatuses ?? []).includes(status) ||\n (options.isQuotaBody?.(outcome.body, status) ?? false)\n\n if (!retryable) return { action: 'return' }\n // Out of attempts on a retryable status: hand the response back rather than\n // inventing an error. The caller owns what a spent 429 means for its run.\n return exhausted ? { action: 'return' } : { action: 'retry', delayMs: wait() }\n}\n\n// ---------------------------------------------------------------------------\n// fetchWithRetry\n// ---------------------------------------------------------------------------\n\n/** The slice of `Response` this module touches. Keeps the module runtime-free. */\nexport interface MinimalResponse {\n status: number\n headers: { get(name: string): string | null }\n clone?(): { text(): Promise<string> }\n text?(): Promise<string>\n}\n\nexport interface FetchWithRetryOptions extends ClassifyOptions {\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n onRetry?(info: { attempt: number; delayMs: number; status: number | null; error: unknown }): void\n /**\n * Renew the credential and return the init for the repeat. Called at most\n * once per request — a refresh loop against a provider that rate-limits its\n * own token endpoint is how an IP gets blocked for an hour.\n */\n onReauth?(): Promise<Record<string, unknown> | void> | Record<string, unknown> | void\n}\n\ntype FetchLike = (input: unknown, init?: unknown) => Promise<MinimalResponse>\n\nasync function peekBody(response: MinimalResponse): Promise<string | null> {\n try {\n const copy = response.clone?.()\n if (copy) return await copy.text()\n return null\n } catch {\n return null\n }\n}\n\n/**\n * One request, retried according to {@link classify}. Returns the last response;\n * it never converts a status into an exception, because which 4xx is fatal is\n * the caller's domain question, not the transport's.\n */\nexport async function fetchWithRetry(\n fetchImpl: FetchLike,\n input: unknown,\n init: unknown,\n options: FetchWithRetryOptions = {},\n): Promise<MinimalResponse> {\n const maxAttempts = options.maxAttempts ?? 4\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n let currentInit = init\n let reauthed = false\n let lastError: unknown\n let lastResponse: MinimalResponse | undefined\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n let response: MinimalResponse | undefined\n let error: unknown\n try {\n response = await fetchImpl(input, currentInit)\n lastResponse = response\n } catch (caught) {\n error = caught\n lastError = caught\n }\n\n const status = response ? response.status : null\n const body = response && statusNeedsBody(status, options) ? await peekBody(response) : null\n const verdict = classify(\n { status, headers: response?.headers ?? null, body, error, attempt, maxAttempts },\n options,\n )\n\n if (verdict.action === 'return' || verdict.action === 'reset-cursor') {\n if (response) return response\n throw lastError ?? new Error('connector request failed')\n }\n\n if (verdict.action === 'fail') {\n throw lastError ?? new ConnectorHttpError(status ?? 0, verdict.reason)\n }\n\n if (verdict.action === 'reauth') {\n // \"Retry after re-authenticating\" is not available to a caller that gave\n // no way to re-authenticate; repeating the request would only spend a\n // second call to be told 401 again.\n if (reauthed || !options.onReauth) return response as MinimalResponse\n reauthed = true\n const next = await options.onReauth()\n if (next) currentInit = next\n options.onRetry?.({ attempt, delayMs: 0, status, error: error ?? null })\n continue\n }\n\n options.onRetry?.({ attempt, delayMs: verdict.delayMs, status, error: error ?? null })\n await sleep(verdict.delayMs)\n }\n\n if (lastResponse) return lastResponse\n throw lastError ?? new Error('connector request failed')\n}\n\n// ---------------------------------------------------------------------------\n// Expiry\n// ---------------------------------------------------------------------------\n\n/**\n * Whether something dated expires within the lead time — a watch channel, an\n * OAuth access token, a refresh token that rotates. Renewing with slack is a\n * requirement and not a nicety for a provider like Bling, whose token endpoint\n * blocks the IP for an hour after 20 calls in a minute: the only safe refresh\n * is the one made days early, never the one made in a loop at the deadline.\n */\nexport function isExpiring(\n expiresAt: string | number | Date | null | undefined,\n options: { now?: number; leadMs?: number } = {},\n): boolean {\n if (!expiresAt) return true\n const expiresAtMs = expiresAt instanceof Date ? expiresAt.getTime() : new Date(expiresAt).getTime()\n if (!Number.isFinite(expiresAtMs)) return true\n const now = options.now ?? Date.now()\n const leadMs = options.leadMs ?? 24 * 60 * 60_000\n return expiresAtMs <= now + leadMs\n}\n\n// ---------------------------------------------------------------------------\n// Pagination — axis A, genuinely one abstraction\n// ---------------------------------------------------------------------------\n// \"This response is a page.\" Google `pageToken`, Bling `pagina=N`, Stripe\n// `starting_after`, GitHub `Link`. A de-para over one hook.\n//\n// The failure this replaces is not hypothetical: FAY-1377 was one page silently\n// dropped, and the pull had been reporting success the whole time.\n\nexport interface PageResult<TItem, TRequest> {\n items?: TItem[]\n /** The request for the next page, or null/undefined when this was the last. */\n next?: TRequest | null\n}\n\nexport interface PaginateOptions<TItem, TRequest> {\n /** The request for page one. */\n first: TRequest\n fetchPage(request: TRequest): Promise<PageResult<TItem, TRequest>>\n /** Hard stop, so a provider that always returns a next token cannot spin. */\n maxPages?: number\n /**\n * Where to restart when a page fails with a `reset-cursor` verdict. Absent =\n * the reset is not available and the error propagates.\n */\n resetTo?(request: TRequest): TRequest | null\n /** Classify a thrown page error. Defaults to reading `error.status`. */\n classifyError?(error: unknown): Verdict\n onCursorReset?(): void\n}\n\nexport interface PaginateResult<TItem> {\n items: TItem[]\n pages: number\n /** Whether the run restarted after the provider rejected the cursor. */\n cursorReset: boolean\n}\n\n/**\n * Walk every page. The contract is that `items` is complete when this returns —\n * a caller that stops at the first page is the FAY-1377 bug, and no connector\n * should have to re-derive the loop to avoid it.\n */\nexport async function paginate<TItem, TRequest>(\n options: PaginateOptions<TItem, TRequest>,\n): Promise<PaginateResult<TItem>> {\n const maxPages = options.maxPages ?? 100\n const classifyError =\n options.classifyError ??\n ((error: unknown): Verdict => {\n const status = (error as { status?: number })?.status\n return status === 410 ? { action: 'reset-cursor' } : { action: 'fail', reason: String(status ?? error) }\n })\n\n let request: TRequest | null = options.first\n let items: TItem[] = []\n let pages = 0\n let cursorReset = false\n\n while (request !== null && request !== undefined) {\n if (pages >= maxPages) throw new Error(`connector pagination limit exceeded (${maxPages} pages)`)\n let page: PageResult<TItem, TRequest>\n try {\n page = await options.fetchPage(request)\n } catch (error) {\n // A stale cursor is not a failure, it is an instruction: drop it and\n // re-list. Once only, so a broken stream cannot spin.\n const verdict = classifyError(error)\n const restart: TRequest | null | undefined =\n verdict.action === 'reset-cursor' && !cursorReset ? options.resetTo?.(request) : null\n if (!restart) throw error\n request = restart\n cursorReset = true\n items = []\n pages = 0\n options.onCursorReset?.()\n continue\n }\n\n pages += 1\n if (page.items?.length) items.push(...page.items)\n request = page.next ?? null\n }\n\n return { items, pages, cursorReset }\n}\n\n// ---------------------------------------------------------------------------\n// The call budget — honoured by the scheduler, not by a sleep in each connector\n// ---------------------------------------------------------------------------\n// Bling allows 3 req/s and 120k/day PER ACCOUNT, counted across every module.\n// That is not an implementation detail; it is what forbids one request per\n// product and therefore what forces `sample` and `backfill` apart.\n//\n// A `sleep(350)` inside the fetch helper cannot express a daily ceiling and\n// cannot be shared between two streams pulling at once. A token bucket the\n// caller must pass through can.\n\nexport interface RateLimitBudget {\n /** Sustained requests per second the provider allows for one account. */\n perSecond?: number\n /** Requests per rolling day for one account, summed across every module. */\n perDay?: number\n}\n\nexport interface RateLimiterOptions {\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n /** Calls already spent today, read from the run history on resume. */\n spentToday?: number\n}\n\nexport class RateLimitExceededError extends Error {\n readonly kind = 'daily-budget'\n constructor(perDay: number) {\n super(`daily call budget of ${perDay} exhausted; the run must resume tomorrow from its cursor`)\n this.name = 'RateLimitExceededError'\n }\n}\n\nexport interface RateLimiter {\n /** Wait until one call fits inside the budget. Throws when the day is spent. */\n take(): Promise<void>\n /** Calls taken since the limiter was created, plus what it resumed with. */\n readonly spent: number\n /** Whether another call fits today at all. */\n readonly exhausted: boolean\n}\n\n/**\n * A token bucket over the declared budget. The clock is injected, so \"3000 SKUs\n * enter without blowing 3 req/s\" is a proof and not an impression.\n */\nexport function createRateLimiter(budget: RateLimitBudget, options: RateLimiterOptions = {}): RateLimiter {\n const now = options.now ?? Date.now\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n const perSecond = budget.perSecond && budget.perSecond > 0 ? budget.perSecond : null\n const perDay = budget.perDay && budget.perDay > 0 ? budget.perDay : null\n // Rounded UP to a whole millisecond. 1000/3 = 333.33 spaces three calls in\n // 999.99ms, which is four inside some one-second window the provider is\n // counting — the budget has to be honoured on the provider's side of the\n // rounding, not on ours.\n const minGapMs = perSecond ? Math.ceil(1_000 / perSecond) : 0\n\n let spent = options.spentToday ?? 0\n // Last permitted departure time. Spacing departures rather than counting\n // arrivals is what makes N concurrent streams share one budget correctly.\n let nextAt = 0\n\n return {\n get spent() { return spent },\n get exhausted() { return perDay !== null && spent >= perDay },\n async take() {\n if (perDay !== null && spent >= perDay) throw new RateLimitExceededError(perDay)\n const at = now()\n const departAt = Math.max(at, nextAt)\n nextAt = departAt + minGapMs\n spent += 1\n const waitMs = departAt - at\n if (waitMs > 0) await sleep(waitMs)\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Window cursors — the client computes the range, the server has no memory\n// ---------------------------------------------------------------------------\n\n/**\n * Split a range into slices no longer than `maxSpanMs`.\n *\n * Bling answers a date filter wider than one year with a 400, so the window is\n * always sliced. Generic because the constraint is not Bling's alone — a\n * provider that caps a range is the normal case, and a connector that discovers\n * the cap in production discovers it as a failed backfill.\n */\nexport function sliceWindow(from: string, to: string, maxSpanMs: number): Array<{ from: string; to: string }> {\n const start = Date.parse(from)\n const end = Date.parse(to)\n if (!Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`sliceWindow: unparseable range ${from}..${to}`)\n if (end <= start) return [{ from, to }]\n if (!Number.isFinite(maxSpanMs) || maxSpanMs <= 0) return [{ from, to }]\n\n const slices: Array<{ from: string; to: string }> = []\n let cursor = start\n while (cursor < end) {\n const next = Math.min(end, cursor + maxSpanMs)\n slices.push({ from: new Date(cursor).toISOString(), to: new Date(next).toISOString() })\n cursor = next\n }\n return slices\n}\n\n/**\n * The next window to ask for, overlapping the one just finished.\n *\n * The overlap is not belt-and-braces: a provider that stamps `updatedAt` at\n * second precision, or writes it a moment before the row is visible, drops\n * whatever landed on the boundary. Re-reading it costs an idempotent upsert.\n */\nexport function advanceWindow(\n cursor: Extract<Cursor, { kind: 'window' }>,\n now: number = Date.now(),\n): Extract<Cursor, { kind: 'window' }> {\n const previousTo = Date.parse(cursor.to)\n const base = Number.isFinite(previousTo) ? previousTo : now\n const from = Math.max(0, base - Math.max(0, cursor.overlapMs))\n return { kind: 'window', from: new Date(from).toISOString(), to: new Date(now).toISOString(), overlapMs: cursor.overlapMs }\n}\n\n/** Serialise a cursor for `plg_sync_runs.cursor`, which is one text column. */\nexport function serializeCursor(cursor: Cursor | null | undefined): string | null {\n if (!cursor) return null\n if (cursor.kind === 'delta') return cursor.token\n if (cursor.kind === 'window') return `${cursor.from}..${cursor.to}`\n return cursor.period\n}\n\n// ---------------------------------------------------------------------------\n// Reconciliation — the whole reason `completeness` exists\n// ---------------------------------------------------------------------------\n\nexport interface MirrorRecord {\n /** The local row's id. */\n id: string\n /** The provider's id it is linked to. */\n externalId: string\n}\n\nexport interface ReconcileInput {\n completeness: Completeness\n /** Provider ids this run actually saw. */\n observed: Iterable<string>\n /** Local rows currently linked to this stream. */\n linked: Iterable<MirrorRecord>\n /** Provider ids the provider explicitly reported as gone. Always honoured. */\n tombstones?: Iterable<string>\n}\n\nexport interface ReconcileResult {\n /** Local ids to cancel. */\n cancel: string[]\n /** Local ids absent from this run that must NOT be touched. */\n keptOutOfRange: string[]\n reason: 'absence-is-deletion' | 'absence-means-nothing'\n}\n\n/**\n * Decide what a run's silence means.\n *\n * Under `authoritative` the provider counted the changes and named the\n * tombstones, so a linked row that did not appear was deleted. Under `additive`\n * the run only ever asked about part of the world — a window, a statement\n * period — and a row that did not appear was simply not asked about. Cancelling\n * it is silent data loss, and it is the failure this whole cursor design exists\n * to make impossible.\n *\n * Explicit tombstones are obeyed under both: a provider that bothers to say\n * \"this one is gone\" is authoritative about that one record regardless of how\n * the run was scoped.\n */\nexport function reconcileMirror(input: ReconcileInput): ReconcileResult {\n const observed = new Set(input.observed)\n const tombstones = new Set(input.tombstones ?? [])\n const authoritative = input.completeness === 'authoritative'\n\n const cancel: string[] = []\n const keptOutOfRange: string[] = []\n\n for (const record of input.linked) {\n if (tombstones.has(record.externalId)) { cancel.push(record.id); continue }\n if (observed.has(record.externalId)) continue\n if (authoritative) cancel.push(record.id)\n else keptOutOfRange.push(record.id)\n }\n\n return { cancel, keptOutOfRange, reason: authoritative ? 'absence-is-deletion' : 'absence-means-nothing' }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/integrations/runtime.ts"],"names":[],"mappings":"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","file":"chunk-3TRR2YLM.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Connector runtime — the mechanics every connector's data plane repeats.\n// ---------------------------------------------------------------------------\n// Lifted out of plugins/plugin-agenda/functions/_shared/google-calendar-runtime.js\n// (FAY-1376): that module was far more generic than its name admitted. Retry,\n// backoff, Retry-After parsing, expiry-with-lead and pagination are HTTP\n// mechanics, not Google's. Only `listAllEventChanges` was genuinely Google's,\n// and it stays there — rewritten on top of `paginate`.\n//\n// This module has NO runtime imports, on purpose. It is built to its own entry\n// (`@fayz-ai/core/connector-runtime`) so a Supabase Edge Function can load it\n// under Deno without dragging React, supabase-js or the plugin registry along.\n// Everything here is pure and transport-injected: `fetch`, the clock and the\n// sleep are parameters, so every decision below is provable under node:test.\n\n// ---------------------------------------------------------------------------\n// Resuming between runs — three things, not one\n// ---------------------------------------------------------------------------\n// A single `cursor: string` collapses a distinction that decides whether data\n// is lost. What separates these is not the field's name, it is whether ABSENCE\n// carries meaning:\n//\n// delta — the SERVER tracks what changed and reports tombstones (Google's\n// syncToken). A linked record that stops appearing was deleted.\n// window — the CLIENT computes a date range; the server has no memory (Bling).\n// A record that stops appearing merely left the window.\n// period — a closed, often sealed statement period (PlugBank). Deletion does\n// not apply.\n//\n// Flatten them and the engine cancels local records that were only out of range.\nexport type Cursor =\n | { kind: 'delta'; token: string | null; restartFrom?: string }\n | { kind: 'window'; from: string; to: string; overlapMs: number }\n | { kind: 'period'; period: string; sealed: boolean }\n\n/** `authoritative` = absence means deleted. `additive` = absence means nothing. */\nexport type Completeness = 'authoritative' | 'additive'\n\n/**\n * What a cursor kind can promise about the records it did NOT return.\n *\n * Only a delta cursor can promise anything: the server counted the changes and\n * named the tombstones. A window is computed by the client against a server\n * with no memory, so the records outside it are not absent — they were never\n * asked for.\n */\nexport function cursorCompleteness(cursor: Cursor): Completeness {\n return cursor.kind === 'delta' ? 'authoritative' : 'additive'\n}\n\n// ---------------------------------------------------------------------------\n// Reading the credential back — the other half of the sink\n// ---------------------------------------------------------------------------\n// The browser writes a tenant's credential to the platform and gets a handle\n// (see `ConnectorCredentialSink` in @fayz-ai/sdk). Something has to read it at\n// run time, and until now nothing did: `POST …/connections/redeem` shipped with\n// no caller at all, so every connector still had to find its provider key\n// somewhere else — which is how three of them ended up with a plaintext column\n// in a pool database.\n//\n// It lives HERE, in the one module already written to be loaded by a Supabase\n// Edge Function under Deno, and not in a per-plugin `_shared/` copy. FAY-1390\n// is about the copies that already exist; adding a third alongside them would\n// be adding the problem to the fix.\n//\n// Redemption takes TWO keys and neither is sufficient alone:\n//\n// the data-plane secret proves the caller is not a browser\n// the runtime-data token proves WHICH TENANT it is acting for\n//\n// The tenant therefore comes off verified claims and is not nameable in the\n// request — a caller holding the secret still cannot reach another tenant's\n// credential, which is the FAY-1375 bug class closed by construction. The\n// consequence for a caller: the runtime token arrives WITH the request being\n// served; an edge function cannot mint one, and must not try.\n\n/**\n * The header a runtime-data token rides in, on its way to a data plane.\n *\n * It lives beside `redeemConnectorCredential` because the two only make sense\n * together: the function cannot mint a token and must forward the one the\n * request carried, so the caller and the callee have to agree on a spelling.\n * Two connectors that each picked their own would fail at exactly the moment a\n * merchant clicks Sincronizar, and would look like a credential problem.\n *\n * Lower-case: an edge function reads `request.headers.get(...)`, which is\n * case-insensitive, but a CORS `Access-Control-Allow-Headers` list is compared\n * against the lower-cased name the browser sends.\n */\nexport const CONNECTOR_RUNTIME_TOKEN_HEADER = 'x-fayz-runtime-token'\n\n/** What redemption returns. `secrets` is the credential — it goes to the provider and nowhere else. */\nexport interface RedeemedConnectorCredential {\n connectionId: string\n connectorSlug: string\n tenantKey: string\n authKind: string\n /**\n * For `api-key` and `mtls`, the fields the merchant typed. For `oauth`, a\n * freshly minted access token — `refresh_token` is removed on the platform\n * side, so the field that could mint further tokens has no route out.\n */\n secrets: Record<string, string>\n /** Present for `oauth`: when the returned access token stops working. */\n expiresAt?: string\n}\n\n/** The credential could not be read. Never carries the secret, the token or the body. */\nexport class ConnectorCredentialUnavailableError extends Error {\n readonly status: number | null\n constructor(message: string, status: number | null = null) {\n super(message)\n this.name = 'ConnectorCredentialUnavailableError'\n this.status = status\n }\n}\n\nexport interface RedeemConnectorCredentialOptions {\n /** Which connector's credential, e.g. 'appmax'. */\n connectorSlug: string\n /**\n * The runtime-data token that came in with the request being served.\n *\n * It is what says which tenant this run is for, and the function has no way\n * to mint one — so it is required for anything serving a signed-in operator.\n * The one alternative is {@link tenantKey}; exactly one of the two must be set.\n */\n runtimeToken?: string\n /**\n * MACHINE mode: name the tenant instead of proving it with a token.\n *\n * For callers a session never reaches — a shopper with no account, a cron job\n * with no user. The redemption secret alone then stands for \"not a browser\",\n * so the platform bounds this path to `MACHINE_REDEEMABLE_CONNECTORS` and\n * audits every use as `machine`.\n *\n * Consequence worth stating plainly: whoever holds the secret can reach any\n * tenant of that project through this door. That is why it only ever lives in\n * an edge function's environment, never in anything a browser loads.\n */\n tenantKey?: string\n /** Defaults to `FAYZ_API_URL` from the environment. */\n apiUrl?: string\n /** Defaults to `FAYZ_PROJECT_ID`. */\n projectId?: string\n /** Defaults to `CONNECTOR_REDEMPTION_SECRET`. */\n redemptionSecret?: string\n /** Defaults to the ambient `fetch`. Injected so the call is provable under node:test. */\n fetchImpl?: (input: string, init: Record<string, unknown>) => Promise<{\n ok: boolean\n status: number\n text(): Promise<string>\n }>\n /** Defaults to `Deno.env.get`, falling back to `process.env`. */\n env?: (name: string) => string | undefined\n}\n\n/**\n * The header a data-plane call carries its runtime token in.\n *\n * A header and not the body: `Authorization` already belongs to the platform\n * the function is deployed on (Supabase checks the caller's JWT there), and a\n * token in a body is a token in whatever logs that body.\n *\n * It is declared ONCE, above, in the module that loads under Deno, because both\n * halves have to name it and only one of them can import the other's: the\n * browser side gets it through `@fayz-ai/core` and the function side through\n * `@fayz-ai/core/connector-runtime`. Written down once, or the second caller\n * spells it differently and neither is wrong at review time — which is exactly\n * what two branches did here, each adding its own `const` with the same value.\n */\n\n/** The three names `provisionConnectorRuntimeEnv` puts into a project's functions. */\nexport const CONNECTOR_REDEMPTION_ENV_KEYS = [\n 'FAYZ_API_URL',\n 'FAYZ_PROJECT_ID',\n 'CONNECTOR_REDEMPTION_SECRET',\n] as const\n\nfunction ambientEnv(name: string): string | undefined {\n const scope = globalThis as {\n Deno?: { env?: { get(key: string): string | undefined } }\n process?: { env?: Record<string, string | undefined> }\n }\n try {\n return scope.Deno?.env?.get(name) ?? scope.process?.env?.[name] ?? undefined\n } catch {\n // Deno throws rather than returning undefined when the permission is absent.\n return undefined\n }\n}\n\n/**\n * `FAYZ_API_URL` is provisioned as `https://host/api` — already one segment in.\n * Accept both spellings so a hand-set variable without it cannot 404 at the one\n * moment a sync needs its key.\n */\nfunction redemptionApiRoot(value: string): string {\n const trimmed = value.trim().replace(/\\/+$/, '')\n return /\\/api$/.test(trimmed) ? trimmed : `${trimmed}/api`\n}\n\n/** Whether something other than the API answered — a tunnel page, a proxy, a login. */\nfunction looksLikeHtml(body: string): boolean {\n return /^\\s*(<!doctype html|<html)/i.test(body ?? '')\n}\n\n/**\n * The platform's own sentence — and ONLY the platform's.\n *\n * A failure body is quoted here, which is a rule the rest of this module spends\n * paragraphs refusing to break: the run row this ends up on is rendered in the\n * merchant's UI, and an error message is the easiest place for a secret to end\n * up. So the quote is bounded to the envelope the Fayz API itself returns\n * (`{ status: 'error', message }`) — a provider's body, a proxy page or any\n * other shape is never repeated, only classified.\n *\n * That narrowness is what the diagnosis needed anyway: \"Runtime data token is\n * for project X, but this call asks about Y\" is our own sentence, and it is the\n * one that turned an unreadable 403 into a one-line fix.\n */\nfunction readRefusal(body: string): string {\n const text = (body ?? '').trim()\n if (!text || !text.startsWith('{')) return ''\n try {\n const parsed = JSON.parse(text) as { status?: unknown; message?: unknown }\n if (parsed.status !== 'error' || typeof parsed.message !== 'string') return ''\n return parsed.message.slice(0, 300)\n } catch {\n return ''\n }\n}\n\n/** The host a redemption was addressed to. Never the path — it carries the ids. */\nexport function redemptionHost(url: string): string {\n try {\n return new URL(url).host\n } catch {\n return 'the platform'\n }\n}\n\n/**\n * What a failed redemption should say. Exported so it can be graded — the value\n * is entirely in what it names.\n *\n * `door` is `'session'` for an operator's token, or the tenant key a machine\n * call named. Both are safe to print; neither the secret nor the token is.\n */\nexport function redemptionFailure(\n connectorSlug: string,\n status: number,\n url: string,\n door: 'session' | string,\n said = '',\n): string {\n const where = redemptionHost(url)\n const who = door === 'session' ? 'a sessão do operador' : `tenantKey ${door}`\n // An HTML body means something ANSWERED FOR the platform — a dead tunnel, a\n // proxy, a login page. Reading that as \"no credential\" sends someone auditing\n // a connection that was never asked about.\n if (looksLikeHtml(said)) {\n return (\n `The platform did not answer at ${where} — a proxy or tunnel page came back instead of the ` +\n `${connectorSlug} credential (${status}). Check FAYZ_API_URL on this project's functions: it ` +\n `points somewhere that is not serving the Fayz API right now`\n )\n }\n const detail = readRefusal(said)\n const base =\n `The platform did not return the ${connectorSlug} credential (${status}) — ${where}, ${who}` +\n (detail ? `: ${detail}` : '')\n if (status !== 404) return base\n // The one status that reads as a bug in the connector and almost never is.\n return (\n `${base}. 404 means that platform has no connected ${connectorSlug} credential for that tenant — ` +\n `most often the connection was authorised against a different environment, or the tenant proving ` +\n `this call is not the one that authorised`\n )\n}\n\n/**\n * Read a tenant's connector credential from the platform, server-side.\n *\n * Everything that could leak is deliberately absent from what this can throw:\n * the response body is never quoted, because the only route that returns a\n * credential is this one and an error message is the easiest place for it to\n * end up.\n */\nexport async function redeemConnectorCredential(\n options: RedeemConnectorCredentialOptions,\n): Promise<RedeemedConnectorCredential> {\n const env = options.env ?? ambientEnv\n const connectorSlug = (options.connectorSlug ?? '').trim()\n const runtimeToken = (options.runtimeToken ?? '').trim()\n const apiUrl = (options.apiUrl ?? env('FAYZ_API_URL') ?? '').trim()\n const projectId = (options.projectId ?? env('FAYZ_PROJECT_ID') ?? '').trim()\n const secret = (options.redemptionSecret ?? env('CONNECTOR_REDEMPTION_SECRET') ?? '').trim()\n\n if (!connectorSlug) throw new ConnectorCredentialUnavailableError('connectorSlug is required to redeem a credential')\n // Unconfigured is a refusal, in every environment. A data plane that answers\n // without its secret is one that was never wired, reporting success.\n if (!apiUrl || !projectId || !secret) {\n throw new ConnectorCredentialUnavailableError(\n `Connector redemption is not configured — expected ${CONNECTOR_REDEMPTION_ENV_KEYS.join(', ')} in the environment`,\n )\n }\n const tenantKey = (options.tenantKey ?? '').trim()\n if (!runtimeToken && !tenantKey) {\n throw new ConnectorCredentialUnavailableError(\n 'No runtime token and no tenantKey on this call, so there is no tenant to redeem a credential for',\n )\n }\n\n const fetchImpl =\n options.fetchImpl ??\n ((input: string, init: Record<string, unknown>) =>\n (globalThis as { fetch: (i: string, n: unknown) => Promise<any> }).fetch(input, init))\n\n const url =\n `${redemptionApiRoot(apiUrl)}/public/projects/${encodeURIComponent(projectId)}` +\n `/connectors/connections/redeem?connectorSlug=${encodeURIComponent(connectorSlug)}`\n\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n // Two keys: the secret says \"not a browser\", the token says \"this tenant\".\n 'x-fayz-connector-secret': secret,\n ...(runtimeToken ? { Authorization: `Bearer ${runtimeToken}` } : {}),\n ...(runtimeToken ? {} : { 'Content-Type': 'application/json' }),\n },\n // Machine mode: no Authorization, tenant named in the body. The platform\n // bounds it to MACHINE_REDEEMABLE_CONNECTORS and audits every use.\n ...(runtimeToken ? {} : { body: JSON.stringify({ tenantKey }) }),\n })\n\n if (!response.ok) {\n // WHICH platform, through WHICH door, and what it actually said. Quoting the\n // body is safe by construction here: the credential only ever comes back on\n // a 200, so a failure body is an error and never a secret.\n const said = await response.text().catch(() => '')\n throw new ConnectorCredentialUnavailableError(\n redemptionFailure(connectorSlug, response.status, url, runtimeToken ? 'session' : tenantKey, said),\n response.status,\n )\n }\n\n let parsed: Partial<RedeemedConnectorCredential> | undefined\n try {\n parsed = JSON.parse(await response.text()) as Partial<RedeemedConnectorCredential>\n } catch {\n throw new ConnectorCredentialUnavailableError(\n `The platform answered ${response.status} with something that is not a credential`,\n response.status,\n )\n }\n\n const secrets = parsed?.secrets\n if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {\n throw new ConnectorCredentialUnavailableError(\n `The ${connectorSlug} connection carries no credential`,\n response.status,\n )\n }\n\n return {\n connectionId: String(parsed?.connectionId ?? ''),\n connectorSlug: String(parsed?.connectorSlug ?? connectorSlug),\n tenantKey: String(parsed?.tenantKey ?? ''),\n authKind: String(parsed?.authKind ?? 'api-key'),\n secrets: secrets as Record<string, string>,\n ...(typeof parsed?.expiresAt === 'string' ? { expiresAt: parsed.expiresAt } : {}),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * A provider answered with a status the caller could not use. `body` is carried\n * because some providers put the real reason there and only there — Google\n * signals quota exhaustion as 403 with `rateLimitExceeded` in the payload, and a\n * classifier that only sees the status cannot tell it from a permission denial.\n */\nexport class ConnectorHttpError extends Error {\n readonly status: number\n readonly retryAfterMs: number | null\n readonly body: string | null\n readonly details: unknown\n\n constructor(\n status: number,\n message: string,\n options: { retryAfterMs?: number | null; body?: string | null; details?: unknown } = {},\n ) {\n super(message)\n this.name = 'ConnectorHttpError'\n this.status = status\n this.retryAfterMs = options.retryAfterMs ?? null\n this.body = options.body ?? null\n this.details = options.details ?? null\n }\n}\n\n// ---------------------------------------------------------------------------\n// Retry-After and backoff\n// ---------------------------------------------------------------------------\n\n/**\n * RFC 7231 `Retry-After`: either delta-seconds or an HTTP-date. Returns\n * milliseconds, or null when the header is absent or unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined, now: number = Date.now()): number | null {\n if (!value) return null\n const seconds = Number(value)\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1_000)\n const at = Date.parse(value)\n return Number.isFinite(at) ? Math.max(0, at - now) : null\n}\n\nexport interface BackoffOptions {\n baseDelayMs?: number\n maxDelayMs?: number\n random?: () => number\n}\n\n/** Exponential backoff with ±25% jitter, capped. Attempt is 1-based. */\nexport function backoffDelay(attempt: number, options: BackoffOptions = {}): number {\n const baseDelayMs = options.baseDelayMs ?? 250\n const maxDelayMs = options.maxDelayMs ?? 5_000\n const random = options.random ?? Math.random\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1))\n return Math.round(exponential * (0.75 + random() * 0.5))\n}\n\n// ---------------------------------------------------------------------------\n// The verdict — retry is not a boolean\n// ---------------------------------------------------------------------------\n// `isRetryable(status)` cannot express that 401 means retry AFTER RE-AUTHENTICATING,\n// 429 means retry AFTER SLEEPING, and Google's 410 means retry AFTER DISCARDING\n// THE CURSOR. Three different recoveries, which is why they lived in three\n// unrelated places and why the 403 case was simply missing.\n//\n// `classify` owns `delayMs` as well as the action. That is what lets one\n// connector honour `Retry-After` while another reads `X-RateLimit-Reset`,\n// without either of them re-implementing the loop.\n\nexport type Verdict =\n /** Hand the response back to the caller — success, or a client error it must see. */\n | { action: 'return' }\n /** Wait, then repeat the same request. */\n | { action: 'retry'; delayMs: number }\n /** The credential is stale: renew it, then repeat the request once. */\n | { action: 'reauth' }\n /** The cursor is gone: discard it and restart the run from scratch. */\n | { action: 'reset-cursor' }\n /** Nothing left to try. */\n | { action: 'fail'; reason: string }\n\n/** What `classify` is shown about one attempt. */\nexport interface AttemptOutcome {\n /** null when the transport itself failed and there is no response. */\n status: number | null\n headers?: { get(name: string): string | null } | null\n /** Response body, when the caller peeked at it. See {@link ClassifyOptions.needsBody}. */\n body?: string | null\n error?: unknown\n /** 1-based. */\n attempt: number\n maxAttempts: number\n}\n\nexport interface ClassifyOptions extends BackoffOptions {\n now?: () => number\n /** Statuses to retry beyond the defaults (408, 425, 429, 5xx). */\n retryableStatuses?: number[]\n /** Statuses meaning \"the cursor is stale, start over\". Google: 410. */\n resetCursorStatuses?: number[]\n /** Statuses meaning \"renew the credential and repeat\". Default: 401. */\n reauthStatuses?: number[]\n /** Where this provider states its wait. Default: `Retry-After` (RFC 7231). */\n readRetryAfter?(headers: { get(name: string): string | null } | null | undefined, now: number): number | null\n /**\n * Whether a 403 is quota exhaustion rather than a permission denial.\n *\n * This is the defect the extraction fixes. `isRetryableGoogleStatus` returned\n * false for 403 — and 403 with `rateLimitExceeded` in the body is exactly how\n * the Google Calendar API reports the quota being blown, the single most\n * characteristic failure of the provider it was written for.\n */\n isQuotaBody?(body: string | null | undefined, status: number): boolean\n /** Statuses whose body `classify` needs. Default: [403], for the case above. */\n needsBody?: number[]\n}\n\nconst DEFAULT_RETRYABLE = new Set([408, 425, 429])\n\n/** Statuses whose body must be read before `classify` can decide. */\nexport function statusNeedsBody(status: number | null, options: ClassifyOptions = {}): boolean {\n if (status === null) return false\n return (options.needsBody ?? [403]).includes(status)\n}\n\n/** The one retry policy. See {@link Verdict}. */\nexport function classify(outcome: AttemptOutcome, options: ClassifyOptions = {}): Verdict {\n const { status, attempt, maxAttempts } = outcome\n const exhausted = attempt >= maxAttempts\n const now = options.now?.() ?? Date.now()\n const readRetryAfter = options.readRetryAfter ?? ((h, at) => parseRetryAfter(h?.get('retry-after'), at))\n const wait = () => readRetryAfter(outcome.headers, now) ?? backoffDelay(attempt, options)\n\n // No response at all. Retrying is the only recovery, and once the attempts are\n // spent there is nothing to hand back — unlike a status, which the caller can\n // still read.\n if (status === null) {\n if (exhausted) return { action: 'fail', reason: String((outcome.error as Error)?.message ?? outcome.error ?? 'request failed') }\n return { action: 'retry', delayMs: backoffDelay(attempt, options) }\n }\n\n if (status < 400) return { action: 'return' }\n\n if ((options.reauthStatuses ?? [401]).includes(status)) {\n return exhausted ? { action: 'return' } : { action: 'reauth' }\n }\n\n if ((options.resetCursorStatuses ?? []).includes(status)) return { action: 'reset-cursor' }\n\n const retryable =\n DEFAULT_RETRYABLE.has(status) ||\n (status >= 500 && status <= 599) ||\n (options.retryableStatuses ?? []).includes(status) ||\n (options.isQuotaBody?.(outcome.body, status) ?? false)\n\n if (!retryable) return { action: 'return' }\n // Out of attempts on a retryable status: hand the response back rather than\n // inventing an error. The caller owns what a spent 429 means for its run.\n return exhausted ? { action: 'return' } : { action: 'retry', delayMs: wait() }\n}\n\n// ---------------------------------------------------------------------------\n// fetchWithRetry\n// ---------------------------------------------------------------------------\n\n/** The slice of `Response` this module touches. Keeps the module runtime-free. */\nexport interface MinimalResponse {\n status: number\n headers: { get(name: string): string | null }\n clone?(): { text(): Promise<string> }\n text?(): Promise<string>\n}\n\nexport interface FetchWithRetryOptions extends ClassifyOptions {\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n onRetry?(info: { attempt: number; delayMs: number; status: number | null; error: unknown }): void\n /**\n * Renew the credential and return the init for the repeat. Called at most\n * once per request — a refresh loop against a provider that rate-limits its\n * own token endpoint is how an IP gets blocked for an hour.\n */\n onReauth?(): Promise<Record<string, unknown> | void> | Record<string, unknown> | void\n}\n\ntype FetchLike = (input: unknown, init?: unknown) => Promise<MinimalResponse>\n\nasync function peekBody(response: MinimalResponse): Promise<string | null> {\n try {\n const copy = response.clone?.()\n if (copy) return await copy.text()\n return null\n } catch {\n return null\n }\n}\n\n/**\n * One request, retried according to {@link classify}. Returns the last response;\n * it never converts a status into an exception, because which 4xx is fatal is\n * the caller's domain question, not the transport's.\n */\nexport async function fetchWithRetry(\n fetchImpl: FetchLike,\n input: unknown,\n init: unknown,\n options: FetchWithRetryOptions = {},\n): Promise<MinimalResponse> {\n const maxAttempts = options.maxAttempts ?? 4\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n let currentInit = init\n let reauthed = false\n let lastError: unknown\n let lastResponse: MinimalResponse | undefined\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n let response: MinimalResponse | undefined\n let error: unknown\n try {\n response = await fetchImpl(input, currentInit)\n lastResponse = response\n } catch (caught) {\n error = caught\n lastError = caught\n }\n\n const status = response ? response.status : null\n const body = response && statusNeedsBody(status, options) ? await peekBody(response) : null\n const verdict = classify(\n { status, headers: response?.headers ?? null, body, error, attempt, maxAttempts },\n options,\n )\n\n if (verdict.action === 'return' || verdict.action === 'reset-cursor') {\n if (response) return response\n throw lastError ?? new Error('connector request failed')\n }\n\n if (verdict.action === 'fail') {\n throw lastError ?? new ConnectorHttpError(status ?? 0, verdict.reason)\n }\n\n if (verdict.action === 'reauth') {\n // \"Retry after re-authenticating\" is not available to a caller that gave\n // no way to re-authenticate; repeating the request would only spend a\n // second call to be told 401 again.\n if (reauthed || !options.onReauth) return response as MinimalResponse\n reauthed = true\n const next = await options.onReauth()\n if (next) currentInit = next\n options.onRetry?.({ attempt, delayMs: 0, status, error: error ?? null })\n continue\n }\n\n options.onRetry?.({ attempt, delayMs: verdict.delayMs, status, error: error ?? null })\n await sleep(verdict.delayMs)\n }\n\n if (lastResponse) return lastResponse\n throw lastError ?? new Error('connector request failed')\n}\n\n// ---------------------------------------------------------------------------\n// Expiry\n// ---------------------------------------------------------------------------\n\n/**\n * Whether something dated expires within the lead time — a watch channel, an\n * OAuth access token, a refresh token that rotates. Renewing with slack is a\n * requirement and not a nicety for a provider like Bling, whose token endpoint\n * blocks the IP for an hour after 20 calls in a minute: the only safe refresh\n * is the one made days early, never the one made in a loop at the deadline.\n */\nexport function isExpiring(\n expiresAt: string | number | Date | null | undefined,\n options: { now?: number; leadMs?: number } = {},\n): boolean {\n if (!expiresAt) return true\n const expiresAtMs = expiresAt instanceof Date ? expiresAt.getTime() : new Date(expiresAt).getTime()\n if (!Number.isFinite(expiresAtMs)) return true\n const now = options.now ?? Date.now()\n const leadMs = options.leadMs ?? 24 * 60 * 60_000\n return expiresAtMs <= now + leadMs\n}\n\n// ---------------------------------------------------------------------------\n// Pagination — axis A, genuinely one abstraction\n// ---------------------------------------------------------------------------\n// \"This response is a page.\" Google `pageToken`, Bling `pagina=N`, Stripe\n// `starting_after`, GitHub `Link`. A de-para over one hook.\n//\n// The failure this replaces is not hypothetical: FAY-1377 was one page silently\n// dropped, and the pull had been reporting success the whole time.\n\nexport interface PageResult<TItem, TRequest> {\n items?: TItem[]\n /** The request for the next page, or null/undefined when this was the last. */\n next?: TRequest | null\n}\n\nexport interface PaginateOptions<TItem, TRequest> {\n /** The request for page one. */\n first: TRequest\n fetchPage(request: TRequest): Promise<PageResult<TItem, TRequest>>\n /** Hard stop, so a provider that always returns a next token cannot spin. */\n maxPages?: number\n /**\n * Where to restart when a page fails with a `reset-cursor` verdict. Absent =\n * the reset is not available and the error propagates.\n */\n resetTo?(request: TRequest): TRequest | null\n /** Classify a thrown page error. Defaults to reading `error.status`. */\n classifyError?(error: unknown): Verdict\n onCursorReset?(): void\n}\n\nexport interface PaginateResult<TItem> {\n items: TItem[]\n pages: number\n /** Whether the run restarted after the provider rejected the cursor. */\n cursorReset: boolean\n}\n\n/**\n * Walk every page. The contract is that `items` is complete when this returns —\n * a caller that stops at the first page is the FAY-1377 bug, and no connector\n * should have to re-derive the loop to avoid it.\n */\nexport async function paginate<TItem, TRequest>(\n options: PaginateOptions<TItem, TRequest>,\n): Promise<PaginateResult<TItem>> {\n const maxPages = options.maxPages ?? 100\n const classifyError =\n options.classifyError ??\n ((error: unknown): Verdict => {\n const status = (error as { status?: number })?.status\n return status === 410 ? { action: 'reset-cursor' } : { action: 'fail', reason: String(status ?? error) }\n })\n\n let request: TRequest | null = options.first\n let items: TItem[] = []\n let pages = 0\n let cursorReset = false\n\n while (request !== null && request !== undefined) {\n if (pages >= maxPages) throw new Error(`connector pagination limit exceeded (${maxPages} pages)`)\n let page: PageResult<TItem, TRequest>\n try {\n page = await options.fetchPage(request)\n } catch (error) {\n // A stale cursor is not a failure, it is an instruction: drop it and\n // re-list. Once only, so a broken stream cannot spin.\n const verdict = classifyError(error)\n const restart: TRequest | null | undefined =\n verdict.action === 'reset-cursor' && !cursorReset ? options.resetTo?.(request) : null\n if (!restart) throw error\n request = restart\n cursorReset = true\n items = []\n pages = 0\n options.onCursorReset?.()\n continue\n }\n\n pages += 1\n if (page.items?.length) items.push(...page.items)\n request = page.next ?? null\n }\n\n return { items, pages, cursorReset }\n}\n\n// ---------------------------------------------------------------------------\n// The call budget — honoured by the scheduler, not by a sleep in each connector\n// ---------------------------------------------------------------------------\n// Bling allows 3 req/s and 120k/day PER ACCOUNT, counted across every module.\n// That is not an implementation detail; it is what forbids one request per\n// product and therefore what forces `sample` and `backfill` apart.\n//\n// A `sleep(350)` inside the fetch helper cannot express a daily ceiling and\n// cannot be shared between two streams pulling at once. A token bucket the\n// caller must pass through can.\n\nexport interface RateLimitBudget {\n /** Sustained requests per second the provider allows for one account. */\n perSecond?: number\n /** Requests per rolling day for one account, summed across every module. */\n perDay?: number\n}\n\nexport interface RateLimiterOptions {\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n /** Calls already spent today, read from the run history on resume. */\n spentToday?: number\n}\n\nexport class RateLimitExceededError extends Error {\n readonly kind = 'daily-budget'\n constructor(perDay: number) {\n super(`daily call budget of ${perDay} exhausted; the run must resume tomorrow from its cursor`)\n this.name = 'RateLimitExceededError'\n }\n}\n\nexport interface RateLimiter {\n /** Wait until one call fits inside the budget. Throws when the day is spent. */\n take(): Promise<void>\n /** Calls taken since the limiter was created, plus what it resumed with. */\n readonly spent: number\n /** Whether another call fits today at all. */\n readonly exhausted: boolean\n}\n\n/**\n * A token bucket over the declared budget. The clock is injected, so \"3000 SKUs\n * enter without blowing 3 req/s\" is a proof and not an impression.\n */\nexport function createRateLimiter(budget: RateLimitBudget, options: RateLimiterOptions = {}): RateLimiter {\n const now = options.now ?? Date.now\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n const perSecond = budget.perSecond && budget.perSecond > 0 ? budget.perSecond : null\n const perDay = budget.perDay && budget.perDay > 0 ? budget.perDay : null\n // Rounded UP to a whole millisecond. 1000/3 = 333.33 spaces three calls in\n // 999.99ms, which is four inside some one-second window the provider is\n // counting — the budget has to be honoured on the provider's side of the\n // rounding, not on ours.\n const minGapMs = perSecond ? Math.ceil(1_000 / perSecond) : 0\n\n let spent = options.spentToday ?? 0\n // Last permitted departure time. Spacing departures rather than counting\n // arrivals is what makes N concurrent streams share one budget correctly.\n let nextAt = 0\n\n return {\n get spent() { return spent },\n get exhausted() { return perDay !== null && spent >= perDay },\n async take() {\n if (perDay !== null && spent >= perDay) throw new RateLimitExceededError(perDay)\n const at = now()\n const departAt = Math.max(at, nextAt)\n nextAt = departAt + minGapMs\n spent += 1\n const waitMs = departAt - at\n if (waitMs > 0) await sleep(waitMs)\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Window cursors — the client computes the range, the server has no memory\n// ---------------------------------------------------------------------------\n\n/**\n * Split a range into slices no longer than `maxSpanMs`.\n *\n * Bling answers a date filter wider than one year with a 400, so the window is\n * always sliced. Generic because the constraint is not Bling's alone — a\n * provider that caps a range is the normal case, and a connector that discovers\n * the cap in production discovers it as a failed backfill.\n */\nexport function sliceWindow(from: string, to: string, maxSpanMs: number): Array<{ from: string; to: string }> {\n const start = Date.parse(from)\n const end = Date.parse(to)\n if (!Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`sliceWindow: unparseable range ${from}..${to}`)\n if (end <= start) return [{ from, to }]\n if (!Number.isFinite(maxSpanMs) || maxSpanMs <= 0) return [{ from, to }]\n\n const slices: Array<{ from: string; to: string }> = []\n let cursor = start\n while (cursor < end) {\n const next = Math.min(end, cursor + maxSpanMs)\n slices.push({ from: new Date(cursor).toISOString(), to: new Date(next).toISOString() })\n cursor = next\n }\n return slices\n}\n\n/**\n * The next window to ask for, overlapping the one just finished.\n *\n * The overlap is not belt-and-braces: a provider that stamps `updatedAt` at\n * second precision, or writes it a moment before the row is visible, drops\n * whatever landed on the boundary. Re-reading it costs an idempotent upsert.\n */\nexport function advanceWindow(\n cursor: Extract<Cursor, { kind: 'window' }>,\n now: number = Date.now(),\n): Extract<Cursor, { kind: 'window' }> {\n const previousTo = Date.parse(cursor.to)\n const base = Number.isFinite(previousTo) ? previousTo : now\n const from = Math.max(0, base - Math.max(0, cursor.overlapMs))\n return { kind: 'window', from: new Date(from).toISOString(), to: new Date(now).toISOString(), overlapMs: cursor.overlapMs }\n}\n\n/** Serialise a cursor for `sync_runs.cursor`, which is one text column. */\nexport function serializeCursor(cursor: Cursor | null | undefined): string | null {\n if (!cursor) return null\n if (cursor.kind === 'delta') return cursor.token\n if (cursor.kind === 'window') return `${cursor.from}..${cursor.to}`\n return cursor.period\n}\n\n// ---------------------------------------------------------------------------\n// Reconciliation — the whole reason `completeness` exists\n// ---------------------------------------------------------------------------\n\nexport interface MirrorRecord {\n /** The local row's id. */\n id: string\n /** The provider's id it is linked to. */\n externalId: string\n}\n\nexport interface ReconcileInput {\n completeness: Completeness\n /** Provider ids this run actually saw. */\n observed: Iterable<string>\n /** Local rows currently linked to this stream. */\n linked: Iterable<MirrorRecord>\n /** Provider ids the provider explicitly reported as gone. Always honoured. */\n tombstones?: Iterable<string>\n}\n\nexport interface ReconcileResult {\n /** Local ids to cancel. */\n cancel: string[]\n /** Local ids absent from this run that must NOT be touched. */\n keptOutOfRange: string[]\n reason: 'absence-is-deletion' | 'absence-means-nothing'\n}\n\n/**\n * Decide what a run's silence means.\n *\n * Under `authoritative` the provider counted the changes and named the\n * tombstones, so a linked row that did not appear was deleted. Under `additive`\n * the run only ever asked about part of the world — a window, a statement\n * period — and a row that did not appear was simply not asked about. Cancelling\n * it is silent data loss, and it is the failure this whole cursor design exists\n * to make impossible.\n *\n * Explicit tombstones are obeyed under both: a provider that bothers to say\n * \"this one is gone\" is authoritative about that one record regardless of how\n * the run was scoped.\n */\nexport function reconcileMirror(input: ReconcileInput): ReconcileResult {\n const observed = new Set(input.observed)\n const tombstones = new Set(input.tombstones ?? [])\n const authoritative = input.completeness === 'authoritative'\n\n const cancel: string[] = []\n const keptOutOfRange: string[] = []\n\n for (const record of input.linked) {\n if (tombstones.has(record.externalId)) { cancel.push(record.id); continue }\n if (observed.has(record.externalId)) continue\n if (authoritative) cancel.push(record.id)\n else keptOutOfRange.push(record.id)\n }\n\n return { cancel, keptOutOfRange, reason: authoritative ? 'absence-is-deletion' : 'absence-means-nothing' }\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Registry } from './chunk-EWKX4VUM.js';
|
|
2
|
+
import { errorMessage } from './chunk-6TVRNKWW.js';
|
|
2
3
|
import { getAllEntities } from './chunk-CU64QI4A.js';
|
|
3
4
|
import * as React from 'react';
|
|
4
5
|
|
|
@@ -603,7 +604,7 @@ async function runExtensionPoint(pointId, input) {
|
|
|
603
604
|
value = await handler.handler(input);
|
|
604
605
|
} catch (cause) {
|
|
605
606
|
const error = new Error(
|
|
606
|
-
`Extension point "${pointId}" handler "${handler.id}" threw: ${
|
|
607
|
+
`Extension point "${pointId}" handler "${handler.id}" threw: ${errorMessage(cause)}`
|
|
607
608
|
);
|
|
608
609
|
if (!isProductionBuild()) throw error;
|
|
609
610
|
console.error(`[extension-points] ${error.message}`);
|
|
@@ -640,7 +641,7 @@ async function runEscapeValve(pointId, input) {
|
|
|
640
641
|
value = await holder.handler(input);
|
|
641
642
|
} catch (cause) {
|
|
642
643
|
const error = new Error(
|
|
643
|
-
`Escape valve "${pointId}" handler "${holder.id}" threw: ${
|
|
644
|
+
`Escape valve "${pointId}" handler "${holder.id}" threw: ${errorMessage(cause)}`
|
|
644
645
|
);
|
|
645
646
|
if (!isProductionBuild()) throw error;
|
|
646
647
|
console.error(`[extension-points] ${error.message}`);
|
|
@@ -1004,8 +1005,10 @@ function createEmptyRuntime(context = EMPTY_CONTEXT) {
|
|
|
1004
1005
|
settingsTabs: [],
|
|
1005
1006
|
widgets: [],
|
|
1006
1007
|
dashboardWidgets: [],
|
|
1008
|
+
headerControls: [],
|
|
1007
1009
|
analyticsCards: [],
|
|
1008
1010
|
onboardingPrograms: [],
|
|
1011
|
+
setupGuides: [],
|
|
1009
1012
|
events: [],
|
|
1010
1013
|
capabilities: [],
|
|
1011
1014
|
aiTools: [],
|
|
@@ -1123,6 +1126,7 @@ function resolvePluginRuntime({
|
|
|
1123
1126
|
const settingsTabs = [];
|
|
1124
1127
|
const widgets = [];
|
|
1125
1128
|
const dashboardWidgets = [];
|
|
1129
|
+
const headerControls = [];
|
|
1126
1130
|
const analyticsCards = [];
|
|
1127
1131
|
const events = [];
|
|
1128
1132
|
const capabilities = [];
|
|
@@ -1132,6 +1136,7 @@ function resolvePluginRuntime({
|
|
|
1132
1136
|
const pluginFeatures = [];
|
|
1133
1137
|
const pluginLimits = [];
|
|
1134
1138
|
const onboardingPrograms = [];
|
|
1139
|
+
const setupGuides = [];
|
|
1135
1140
|
for (const plugin of activePlugins) {
|
|
1136
1141
|
routes.push(...plugin.routes.map((r) => ({ ...r, plugin })));
|
|
1137
1142
|
navigation.push(...plugin.navigation.map((entry, i) => ({
|
|
@@ -1140,6 +1145,7 @@ function resolvePluginRuntime({
|
|
|
1140
1145
|
icon: entry.icon ?? plugin.icon
|
|
1141
1146
|
})));
|
|
1142
1147
|
settingsTabs.push(...plugin.settingsTabs);
|
|
1148
|
+
headerControls.push(...(plugin.headerControls ?? []).map((c) => ({ ...c, pluginId: plugin.id })));
|
|
1143
1149
|
capabilities.push(...plugin.capabilities ?? []);
|
|
1144
1150
|
events.push(...(plugin.events ?? []).map((e) => ({
|
|
1145
1151
|
...e,
|
|
@@ -1160,6 +1166,14 @@ function resolvePluginRuntime({
|
|
|
1160
1166
|
}
|
|
1161
1167
|
if (plugin.declaredFeatures) pluginFeatures.push(...plugin.declaredFeatures);
|
|
1162
1168
|
if (plugin.declaredLimits) pluginLimits.push(...plugin.declaredLimits);
|
|
1169
|
+
for (const guide of plugin.setupGuides ?? []) {
|
|
1170
|
+
setupGuides.push({
|
|
1171
|
+
...guide,
|
|
1172
|
+
role: guide.role ?? plugin.defaultAgentRole,
|
|
1173
|
+
pluginId: plugin.id,
|
|
1174
|
+
pluginName: plugin.name
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1163
1177
|
if (plugin.onboarding?.program) {
|
|
1164
1178
|
const program = plugin.onboarding.program;
|
|
1165
1179
|
onboardingPrograms.push({
|
|
@@ -1214,7 +1228,7 @@ function resolvePluginRuntime({
|
|
|
1214
1228
|
onboardingPrograms.sort((a, b) => a.order - b.order);
|
|
1215
1229
|
dashboardWidgets.sort((a, b) => a.order - b.order);
|
|
1216
1230
|
analyticsCards.sort((a, b) => a.order - b.order);
|
|
1217
|
-
return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, analyticsCards, capabilities, events, aiTools, issues, registries, connectorsByHost, pluginFeatures, pluginLimits, onboardingPrograms };
|
|
1231
|
+
return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, headerControls, analyticsCards, capabilities, events, aiTools, issues, registries, connectorsByHost, pluginFeatures, pluginLimits, onboardingPrograms, setupGuides };
|
|
1218
1232
|
}
|
|
1219
1233
|
function getWidgetsForZone(runtime, zone, contextOverrides) {
|
|
1220
1234
|
const ctx = { ...runtime.context, ...contextOverrides };
|
|
@@ -1260,5 +1274,5 @@ function useConnectorsForPlugin(hostPluginId) {
|
|
|
1260
1274
|
}
|
|
1261
1275
|
|
|
1262
1276
|
export { BLOCK_SETTING_TYPES, PLUGIN_API_VERSION, PluginRuntimeProvider, auditBlocks, blockRegistry, buildEntityDomainEvents, checkAgainstSchema, checkBlockConstraints, checkBlockContext, checkNodePlacement, clearExtensionPoints, clearHandles, clearLoaders, componentRegistry, dataNeedKey, defaultPropsFromSettings, defineBlock, defineExtensionPoint, defineLoader, definePlugin, describeBlock, describeBlocks, describeExtensionPoints, describeLoaders, entityEventKey, extensionAfterHooks, extensionPointRegistry, formatSchemaIssues, getAnalyticsCards, getBlock, getBlockMeta, getComponent, getDashboardWidgets, getHandle, getMetric, getPage, getPlugin, getPluginFactory, getScaffold, getWidgetsForZone, handleNameFor, handleRegistry, hasExtensionPoint, hasHandle, hasLoader, isBlockEnabledOn, isLoaderParamRef, isLoaderPropRef, listBlocks, listExtensionHandlers, listExtensionPoints, listHandles, listLoaders, listMetrics, listPluginFactories, listPlugins, loaderRegistry, metricRegistry, pageRegistry, pluginFactoryRegistry, pluginRegistry, registerBlock, registerComponent, registerExtensionHandler, registerHandle, registerMetric, registerPage, registerPlugin, registerPluginFactory, registerScaffold, repairBlockTree, resolveHandle, resolveLoader, resolvePluginComponent, resolvePluginRuntime, runEscapeValve, runExtensionPoint, scaffoldRegistry, settingsToJsonSchema, summarizeBlockAudit, unregisterExtensionHandler, useConnectorsForPlugin, usePluginRuntime, usePluginRuntimeOptional };
|
|
1263
|
-
//# sourceMappingURL=chunk-
|
|
1264
|
-
//# sourceMappingURL=chunk-
|
|
1277
|
+
//# sourceMappingURL=chunk-4SPEISNL.js.map
|
|
1278
|
+
//# sourceMappingURL=chunk-4SPEISNL.js.map
|