@fayz-ai/core 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/engine.d.ts +31 -1
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/index.d.ts +1 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +24 -3
- package/dist/access/index.js.map +1 -1
- package/dist/app/app-config.d.ts +101 -0
- package/dist/app/app-config.d.ts.map +1 -0
- package/dist/app/service-client.d.ts +69 -0
- package/dist/app/service-client.d.ts.map +1 -0
- package/dist/{chunk-S5FOFKR4.js → chunk-2ZAUJVYP.js} +103 -24
- package/dist/chunk-2ZAUJVYP.js.map +1 -0
- package/dist/{chunk-KFBEKFII.js → chunk-BCI743RH.js} +37 -4
- package/dist/chunk-BCI743RH.js.map +1 -0
- package/dist/{chunk-IKZR3WSK.js → chunk-KJUD6JA5.js} +3 -3
- package/dist/{chunk-IKZR3WSK.js.map → chunk-KJUD6JA5.js.map} +1 -1
- package/dist/{chunk-7GJTLBG3.js → chunk-OHH26S4N.js} +3 -3
- package/dist/{chunk-7GJTLBG3.js.map → chunk-OHH26S4N.js.map} +1 -1
- package/dist/chunk-RCMFU3RZ.js +13 -0
- package/dist/chunk-RCMFU3RZ.js.map +1 -0
- package/dist/{chunk-RRYFXUQX.js → chunk-UMIZDILP.js} +7 -3
- package/dist/chunk-UMIZDILP.js.map +1 -0
- package/dist/{chunk-X4FRQR2R.js → chunk-ZIB6Y7TE.js} +153 -8
- package/dist/chunk-ZIB6Y7TE.js.map +1 -0
- package/dist/data/archetype.d.ts +5 -0
- package/dist/data/archetype.d.ts.map +1 -1
- package/dist/data/cached.d.ts +17 -2
- package/dist/data/cached.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +2 -2
- package/dist/data/missing-objects.d.ts +14 -0
- package/dist/data/missing-objects.d.ts.map +1 -0
- package/dist/data/offline.d.ts +10 -1
- package/dist/data/offline.d.ts.map +1 -1
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/supabase.d.ts +5 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/unit-scope.d.ts +48 -0
- package/dist/data/unit-scope.d.ts.map +1 -0
- package/dist/events/envelope.d.ts.map +1 -1
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/log.d.ts.map +1 -1
- package/dist/events/useDataVersion.d.ts +28 -0
- package/dist/events/useDataVersion.d.ts.map +1 -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 +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +653 -45
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.js +2 -2
- package/dist/lib/debug.d.ts +12 -0
- package/dist/lib/debug.d.ts.map +1 -0
- package/dist/lib/unit.d.ts +20 -0
- package/dist/lib/unit.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +45 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/integration-surface.d.ts +16 -0
- package/dist/manifest/integration-surface.d.ts.map +1 -0
- package/dist/messaging/index.d.ts +5 -2
- package/dist/messaging/index.d.ts.map +1 -1
- package/dist/messaging/phone.d.ts +32 -0
- package/dist/messaging/phone.d.ts.map +1 -0
- package/dist/messaging/provider-template.d.ts +122 -0
- package/dist/messaging/provider-template.d.ts.map +1 -0
- package/dist/messaging/registry.d.ts +12 -1
- package/dist/messaging/registry.d.ts.map +1 -1
- package/dist/messaging/types.d.ts +32 -0
- package/dist/messaging/types.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/search/engine.d.ts.map +1 -1
- package/dist/{shell-translations.pt-br-PTOWYXNA.js → shell-translations.pt-br-HD4F3QKU.js} +118 -7
- package/dist/shell-translations.pt-br-HD4F3QKU.js.map +1 -0
- package/dist/testing/index.js +3 -3
- package/dist/types/agents.d.ts +97 -0
- package/dist/types/agents.d.ts.map +1 -0
- package/dist/types/analytics.d.ts +15 -0
- package/dist/types/analytics.d.ts.map +1 -1
- package/dist/types/auth.d.ts +9 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/crud.d.ts +38 -0
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/org.d.ts +10 -0
- package/dist/types/org.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +97 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-KFBEKFII.js.map +0 -1
- package/dist/chunk-RRYFXUQX.js.map +0 -1
- package/dist/chunk-S5FOFKR4.js.map +0 -1
- package/dist/chunk-X4FRQR2R.js.map +0 -1
- package/dist/shell-translations.pt-br-PTOWYXNA.js.map +0 -1
package/dist/access/engine.d.ts
CHANGED
|
@@ -12,6 +12,19 @@ export interface AccessSubject {
|
|
|
12
12
|
profile: PermissionProfile | null;
|
|
13
13
|
/** The tenant's currently-active plan. */
|
|
14
14
|
plan: Plan | null;
|
|
15
|
+
/**
|
|
16
|
+
* Extra role a person has INSIDE a given unit, keyed by unit id.
|
|
17
|
+
*
|
|
18
|
+
* Additive, never subtractive: the org role applies everywhere and a unit role
|
|
19
|
+
* only adds inside its unit. That choice is what makes this shippable — the
|
|
20
|
+
* answer without a unit is a strict SUBSET of the answer with one, so every
|
|
21
|
+
* existing call site keeps its exact meaning and none of them had to change.
|
|
22
|
+
* The alternative (a unit role that overrides) can take access away by
|
|
23
|
+
* accident, and every screen would have to explain which role won.
|
|
24
|
+
*/
|
|
25
|
+
unitProfiles?: Record<string, PermissionProfile>;
|
|
26
|
+
/** The unit the person is looking at, used when a check does not name one. */
|
|
27
|
+
activeUnitId?: string;
|
|
15
28
|
}
|
|
16
29
|
/**
|
|
17
30
|
* The org owner has every permission implicitly. Owner is represented by the
|
|
@@ -41,6 +54,21 @@ export declare function profileHasSystemPermission(profile: PermissionProfile |
|
|
|
41
54
|
* open rather than silently locking.
|
|
42
55
|
*/
|
|
43
56
|
export declare function isEntitledByPlan(plan: Plan | null, feature: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Union two profiles. Grants merge per feature, system permissions merge, and
|
|
59
|
+
* the result is never smaller than either input — the additive rule, in code.
|
|
60
|
+
*
|
|
61
|
+
* `manage` is not expanded here: {@link profileHasPermission} already treats it
|
|
62
|
+
* as satisfying any action, so expanding it would be a second implementation of
|
|
63
|
+
* the same rule waiting to disagree with the first.
|
|
64
|
+
*/
|
|
65
|
+
export declare function mergeProfiles(base: PermissionProfile | null, overlay: PermissionProfile | null): PermissionProfile | null;
|
|
66
|
+
/**
|
|
67
|
+
* The profile that applies where the person is standing: their org role, plus
|
|
68
|
+
* whatever the unit adds. With no unit — or no binding in it — this is exactly
|
|
69
|
+
* the org role, which is why nothing that predates units changes behaviour.
|
|
70
|
+
*/
|
|
71
|
+
export declare function effectiveProfileFor(subject: AccessSubject, unitId?: string): PermissionProfile | null;
|
|
44
72
|
/**
|
|
45
73
|
* The single access decision: role FIRST, then plan.
|
|
46
74
|
*
|
|
@@ -50,7 +78,9 @@ export declare function isEntitledByPlan(plan: Plan | null, feature: string): bo
|
|
|
50
78
|
* 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's
|
|
51
79
|
* decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.
|
|
52
80
|
*/
|
|
53
|
-
export declare function resolveAccess(subject: AccessSubject, feature: string, action?: PermissionAction
|
|
81
|
+
export declare function resolveAccess(subject: AccessSubject, feature: string, action?: PermissionAction, options?: {
|
|
82
|
+
unitId?: string;
|
|
83
|
+
}): AccessDecision;
|
|
54
84
|
/**
|
|
55
85
|
* Pure numeric side of a limit: given the plan cap, whether a countable
|
|
56
86
|
* declaration exists, and the current usage, derive the limit math. Unlimited
|
|
@@ -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;
|
|
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;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAeT;AAED;;;;;GAKG;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;;;;;;;;GAQG;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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { isOwnerProfile, profileHasPermission, profileHasSystemPermission, isEntitledByPlan, resolveAccess, resolveLimit, } from './engine';
|
|
1
|
+
export { isOwnerProfile, profileHasPermission, profileHasSystemPermission, isEntitledByPlan, resolveAccess, resolveLimit, mergeProfiles, effectiveProfileFor, } from './engine';
|
|
2
2
|
export type { DenyReason, AccessDecision, AccessSubject } from './engine';
|
|
3
3
|
export { UPGRADE_URL, agentDenial } from './denial';
|
|
4
4
|
export type { AgentDenial, AgentDenialLimit, AgentGuardResult } from './denial';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,aAAa,EACb,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,mBAAmB,GACpB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAEzE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA"}
|
package/dist/access/index.js
CHANGED
|
@@ -18,8 +18,29 @@ function profileHasSystemPermission(profile, permission) {
|
|
|
18
18
|
function isEntitledByPlan(plan, feature) {
|
|
19
19
|
return plan?.entitlements?.features?.[feature] !== false;
|
|
20
20
|
}
|
|
21
|
-
function
|
|
22
|
-
if (!
|
|
21
|
+
function mergeProfiles(base, overlay) {
|
|
22
|
+
if (!overlay) return base;
|
|
23
|
+
if (!base) return overlay;
|
|
24
|
+
if (isOwnerProfile(base)) return base;
|
|
25
|
+
const grants = { ...base.grants };
|
|
26
|
+
for (const [feature, actions] of Object.entries(overlay.grants ?? {})) {
|
|
27
|
+
const merged = /* @__PURE__ */ new Set([...grants[feature] ?? [], ...actions]);
|
|
28
|
+
grants[feature] = [...merged];
|
|
29
|
+
}
|
|
30
|
+
const system = /* @__PURE__ */ new Set([...base.systemPermissions ?? [], ...overlay.systemPermissions ?? []]);
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
grants,
|
|
34
|
+
systemPermissions: system.size > 0 ? [...system] : void 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function effectiveProfileFor(subject, unitId) {
|
|
38
|
+
if (!unitId) return subject.profile;
|
|
39
|
+
return mergeProfiles(subject.profile, subject.unitProfiles?.[unitId] ?? null);
|
|
40
|
+
}
|
|
41
|
+
function resolveAccess(subject, feature, action, options) {
|
|
42
|
+
const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId);
|
|
43
|
+
if (!profileHasPermission(profile, feature, action)) {
|
|
23
44
|
return { allowed: false, reason: "role" };
|
|
24
45
|
}
|
|
25
46
|
if (!isEntitledByPlan(subject.plan, feature)) {
|
|
@@ -51,6 +72,6 @@ function mergeLimitDeclarations(...layers) {
|
|
|
51
72
|
return Array.from(byKey.values());
|
|
52
73
|
}
|
|
53
74
|
|
|
54
|
-
export { UPGRADE_URL, agentDenial, isEntitledByPlan, isOwnerProfile, mergeLimitDeclarations, profileHasPermission, profileHasSystemPermission, resolveAccess, resolveLimit };
|
|
75
|
+
export { UPGRADE_URL, agentDenial, effectiveProfileFor, isEntitledByPlan, isOwnerProfile, mergeLimitDeclarations, mergeProfiles, profileHasPermission, profileHasSystemPermission, resolveAccess, resolveLimit };
|
|
55
76
|
//# sourceMappingURL=index.js.map
|
|
56
77
|
//# sourceMappingURL=index.js.map
|
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\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 * 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): AccessDecision {\n if (!profileHasPermission(subject.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 */\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"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { TenantPluginBinding } from '../types/plugins';
|
|
2
|
+
/** One activation row: a plugin, or one facet of it. */
|
|
3
|
+
export interface AppConfigPlugin {
|
|
4
|
+
plugin: string;
|
|
5
|
+
/** null for the plugin itself; a name for one of its parts. */
|
|
6
|
+
facet: string | null;
|
|
7
|
+
status: 'active' | 'disabled';
|
|
8
|
+
/** Who turned it on — manual, plan, trial, agent, default. */
|
|
9
|
+
source: string;
|
|
10
|
+
config: Record<string, unknown>;
|
|
11
|
+
/** When a trial or courtesy ends; null is forever. */
|
|
12
|
+
expires_at: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** The tenant's difference over the menu the plugins offer. */
|
|
15
|
+
export interface AppConfigNavEntry {
|
|
16
|
+
key: string;
|
|
17
|
+
hidden: boolean;
|
|
18
|
+
label: string | null;
|
|
19
|
+
section: 'main' | 'secondary' | 'settings' | null;
|
|
20
|
+
position: number | null;
|
|
21
|
+
}
|
|
22
|
+
export interface AppConfig {
|
|
23
|
+
tenant_id: string | null;
|
|
24
|
+
branding?: Record<string, unknown>;
|
|
25
|
+
settings?: Record<string, unknown>;
|
|
26
|
+
plugins: AppConfigPlugin[];
|
|
27
|
+
nav: AppConfigNavEntry[];
|
|
28
|
+
permissions?: string[];
|
|
29
|
+
generated_at?: string;
|
|
30
|
+
}
|
|
31
|
+
/** What a caller with no resolvable tenant gets: nothing, legibly. */
|
|
32
|
+
export declare const EMPTY_APP_CONFIG: AppConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Reads the whole configuration in one round trip.
|
|
35
|
+
*
|
|
36
|
+
* Never throws: a shell that cannot render because the configuration call
|
|
37
|
+
* failed is worse than a shell that renders the defaults. The failure is
|
|
38
|
+
* returned as an empty configuration, which is the same thing the RPC returns
|
|
39
|
+
* for a caller with no tenant — and both are visible in one place rather than
|
|
40
|
+
* as a blank screen.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchAppConfig(client: unknown, options?: {
|
|
43
|
+
tenantId?: string;
|
|
44
|
+
force?: boolean;
|
|
45
|
+
}): Promise<AppConfig>;
|
|
46
|
+
/** Drops what is cached — called after any write through the config RPCs. */
|
|
47
|
+
export declare function invalidateAppConfig(tenantId?: string): void;
|
|
48
|
+
export declare function normalizeAppConfig(raw: Partial<AppConfig>): AppConfig;
|
|
49
|
+
/**
|
|
50
|
+
* The activation rows as the plugin runtime already understands them.
|
|
51
|
+
*
|
|
52
|
+
* `resolvePluginRuntime` has accepted `tenantPlugins: TenantPluginBinding[]`
|
|
53
|
+
* since it was written and nothing ever supplied them from anywhere but a file.
|
|
54
|
+
* This is that supply — facet rows are folded into their plugin's config under
|
|
55
|
+
* `facets`, because a facet is a part of a plugin and not a plugin.
|
|
56
|
+
*/
|
|
57
|
+
export declare function toTenantPluginBindings(config: AppConfig): TenantPluginBinding[];
|
|
58
|
+
/**
|
|
59
|
+
* Applies the tenant's menu composition to what the plugins offer.
|
|
60
|
+
*
|
|
61
|
+
* Only the difference is stored, so an entry with no row here keeps exactly the
|
|
62
|
+
* position and label its plugin gave it — which is what lets a plugin ship a new
|
|
63
|
+
* entry and have it reach every tenant.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyNavComposition<T extends {
|
|
66
|
+
key: string;
|
|
67
|
+
label?: string;
|
|
68
|
+
section?: string;
|
|
69
|
+
position?: number;
|
|
70
|
+
}>(entries: T[], nav: AppConfigNavEntry[]): T[];
|
|
71
|
+
/** The keys a generated app must NOT ship in a file: they are per-tenant. */
|
|
72
|
+
export declare const TENANT_OWNED_MANIFEST_KEYS: readonly ["theme", "branding", "locale"];
|
|
73
|
+
export interface ManifestLike {
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
theme?: Record<string, unknown>;
|
|
76
|
+
branding?: Record<string, unknown>;
|
|
77
|
+
locale?: Record<string, unknown>;
|
|
78
|
+
surfaces?: Record<string, {
|
|
79
|
+
plugins?: Array<{
|
|
80
|
+
id: string;
|
|
81
|
+
config?: Record<string, unknown>;
|
|
82
|
+
}>;
|
|
83
|
+
} & Record<string, unknown>>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Folds the tenant's configuration over the bundle's manifest.
|
|
87
|
+
*
|
|
88
|
+
* What stays the manifest's: the surfaces, their scaffolds, and which plugin
|
|
89
|
+
* ids the bundle can load at all — an import-graph fact that no database row can
|
|
90
|
+
* change, because the code either shipped or it did not.
|
|
91
|
+
*
|
|
92
|
+
* What the database decides: branding, locale, and whether each of those
|
|
93
|
+
* plugins is on for this tenant, with its settings. That is the whole point of
|
|
94
|
+
* ADR 0023 — eleven customers stop needing eleven repositories, and turning a
|
|
95
|
+
* feature on stops being a deploy.
|
|
96
|
+
*
|
|
97
|
+
* An empty config changes nothing, so a pool that has not run migration 150 yet
|
|
98
|
+
* renders exactly what it rendered before.
|
|
99
|
+
*/
|
|
100
|
+
export declare function applyAppConfigToManifest<T extends ManifestLike>(manifest: T, config: AppConfig): T;
|
|
101
|
+
//# sourceMappingURL=app-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/app/app-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Why a call did not produce a value. Each one has a different right answer. */
|
|
2
|
+
export type ServiceRefusal =
|
|
3
|
+
/** The service is not registered, or was turned off. */
|
|
4
|
+
'unknown_service'
|
|
5
|
+
/** No tenant in scope — a caller that cannot be placed. */
|
|
6
|
+
| 'no_tenant'
|
|
7
|
+
/** The caller lacks the permission the service requires. */
|
|
8
|
+
| 'forbidden'
|
|
9
|
+
/** The tenant has not contracted it: an upsell, not an error. */
|
|
10
|
+
| 'not_contracted'
|
|
11
|
+
/** It did not answer in time. */
|
|
12
|
+
| 'timeout'
|
|
13
|
+
/** It answered with a failure. */
|
|
14
|
+
| 'failed'
|
|
15
|
+
/** There is nothing to call through (no client, no session). */
|
|
16
|
+
| 'unavailable';
|
|
17
|
+
export interface ServiceSuccess<T> {
|
|
18
|
+
ok: true;
|
|
19
|
+
data: T;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ServiceFailure {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: ServiceRefusal;
|
|
25
|
+
/** What the user can still do, as the service itself declared it. */
|
|
26
|
+
manualFallback?: string;
|
|
27
|
+
/** For `not_contracted`: what to offer. */
|
|
28
|
+
entitlement?: string;
|
|
29
|
+
plugin?: string;
|
|
30
|
+
/** For `forbidden`: what would be needed. */
|
|
31
|
+
permission?: string;
|
|
32
|
+
message?: string;
|
|
33
|
+
}
|
|
34
|
+
export type ServiceResult<T> = ServiceSuccess<T> | ServiceFailure;
|
|
35
|
+
export interface CallServiceOptions {
|
|
36
|
+
/** Overrides the timeout the registry declares for this service. */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
/** The edge function that performs the HTTP leg. Defaults to 'service-router'. */
|
|
39
|
+
router?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Calls on behalf of a tenant with no session — the event consumer's 2am
|
|
42
|
+
* reaction (#278). Requires a service-role client; a browser client passing
|
|
43
|
+
* this is refused by the router, not by trust.
|
|
44
|
+
*/
|
|
45
|
+
tenantId?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Calls a service by name.
|
|
49
|
+
*
|
|
50
|
+
* Two round trips by design. The first asks the database whether the call is
|
|
51
|
+
* allowed — which costs nothing and answers the same way everywhere, so a
|
|
52
|
+
* refusal never leaves the cluster and an unentitled tenant never reaches the
|
|
53
|
+
* service at all. Only an allowed call makes the second.
|
|
54
|
+
*/
|
|
55
|
+
export declare function callService<T = unknown>(client: unknown, name: string, payload?: Record<string, unknown>, options?: CallServiceOptions): Promise<ServiceResult<T>>;
|
|
56
|
+
/**
|
|
57
|
+
* Calls a service for a tenant with no user present — the worker draining the
|
|
58
|
+
* event log (#277) reacting to a fact at 2am (#278).
|
|
59
|
+
*
|
|
60
|
+
* Same door, same refusals, same never-throws contract. What differs is who is
|
|
61
|
+
* asking: there is no person, so there is no permission to check, and the
|
|
62
|
+
* entitlement rule carries the whole decision because a tenant's contract is a
|
|
63
|
+
* property of the tenant rather than of who is looking.
|
|
64
|
+
*
|
|
65
|
+
* The client must hold the service role. A browser client that passes a tenant
|
|
66
|
+
* is refused by the router, which checks rather than trusts.
|
|
67
|
+
*/
|
|
68
|
+
export declare function callServiceForTenant<T = unknown>(client: unknown, tenantId: string, name: string, payload?: Record<string, unknown>, options?: Omit<CallServiceOptions, 'tenantId'>): Promise<ServiceResult<T>>;
|
|
69
|
+
//# sourceMappingURL=service-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-client.d.ts","sourceRoot":"","sources":["../../src/app/service-client.ts"],"names":[],"mappings":"AAgBA,iFAAiF;AACjF,MAAM,MAAM,cAAc;AACxB,wDAAwD;AACtD,iBAAiB;AACnB,2DAA2D;GACzD,WAAW;AACb,4DAA4D;GAC1D,WAAW;AACb,iEAAiE;GAC/D,gBAAgB;AAClB,iCAAiC;GAC/B,SAAS;AACX,kCAAkC;GAChC,QAAQ;AACV,gEAAgE;GAC9D,aAAa,CAAA;AAEjB,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,EAAE,EAAE,IAAI,CAAA;IACR,IAAI,EAAE,CAAC,CAAA;IACP,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,cAAc,CAAA;IACtB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAA;AAajE,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,GAAG,OAAO,EAC3C,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CA6D3B;AA2CD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,OAAO,EAC9C,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAM,GACjD,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAE3B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { globalCache, getActiveTenantId, getSupabaseClientOptional, parseRangeFilter, matchesRange, applyRangeToQuery, isFilterObject, stableKey, DEFAULT_CACHE_TTL, createSupabaseProvider } from './chunk-
|
|
1
|
+
import { globalCache, getActiveTenantId, getSupabaseClientOptional, parseRangeFilter, matchesRange, applyUnitScope, applyRangeToQuery, isFilterObject, stableKey, DEFAULT_CACHE_TTL, getScopedResource, createSupabaseProvider, resolveUnitScope, clearGlobalCache } from './chunk-BCI743RH.js';
|
|
2
2
|
import { declaredCustomFields, validateEntityRecordData } from './chunk-EWKX4VUM.js';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
@@ -303,14 +303,15 @@ var ARCHETYPE_CONFIG = {
|
|
|
303
303
|
};
|
|
304
304
|
var CUSTOM_FIELDS_COLUMN = "custom_fields";
|
|
305
305
|
var CUSTOM_FIELD_FILTER_PREFIX = "custom.";
|
|
306
|
+
var UNIT_COLUMN = "unit_id";
|
|
306
307
|
var ARCHETYPE_COLUMNS = {
|
|
307
|
-
person: /* @__PURE__ */ new Set(["name", "email", "phone", "document_number", "avatar_url", "date_of_birth", "address", "city", "state", "country", "postal_code", "tags", "is_active", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
308
|
-
category: /* @__PURE__ */ new Set(["name", "slug", "parent_id", "icon", "color", "sort_order", "is_active", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
309
|
-
product: /* @__PURE__ */ new Set(["name", "description", "sku", "price", "cost", "currency", "unit", "image_url", "stock", "min_stock", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN]),
|
|
310
|
-
service: /* @__PURE__ */ new Set(["name", "description", "price", "cost", "currency", "duration_minutes", "image_url", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN]),
|
|
308
|
+
person: /* @__PURE__ */ new Set(["name", "email", "phone", "document_number", "avatar_url", "date_of_birth", "address", "city", "state", "country", "postal_code", "tags", "is_active", "is_bookable", "notes", "metadata", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
|
|
309
|
+
category: /* @__PURE__ */ new Set(["name", "slug", "parent_id", "icon", "color", "sort_order", "is_active", "metadata", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
|
|
310
|
+
product: /* @__PURE__ */ new Set(["name", "description", "sku", "price", "cost", "currency", "unit", "image_url", "stock", "min_stock", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
|
|
311
|
+
service: /* @__PURE__ */ new Set(["name", "description", "price", "cost", "currency", "duration_minutes", "image_url", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
|
|
311
312
|
location: /* @__PURE__ */ new Set(["name", "email", "phone", "address", "city", "state", "country", "postal_code", "is_headquarters", "is_active", "tags", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
312
313
|
order: /* @__PURE__ */ new Set(["reference_number", "status", "party_id", "assignee_id", "location_id", "subtotal", "discount", "tax", "total", "currency", "due_at", "completed_at", "notes", "tags", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
313
|
-
transaction: /* @__PURE__ */ new Set(["order_id", "party_id", "amount", "currency", "payment_method", "reference", "status", "transacted_at", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
|
|
314
|
+
transaction: /* @__PURE__ */ new Set(["order_id", "party_id", "amount", "currency", "payment_method", "reference", "status", "transacted_at", "notes", "metadata", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
|
|
314
315
|
schedule: /* @__PURE__ */ new Set(["assignee_id", "location_id", "day_of_week", "specific_date", "starts_at", "ends_at", "is_active", "metadata", CUSTOM_FIELDS_COLUMN])
|
|
315
316
|
};
|
|
316
317
|
function camelToSnake(str) {
|
|
@@ -338,11 +339,14 @@ function createArchetypeProvider(config) {
|
|
|
338
339
|
function coreClient() {
|
|
339
340
|
return getClient2();
|
|
340
341
|
}
|
|
342
|
+
function unitScope() {
|
|
343
|
+
return resolveUnitScope(config);
|
|
344
|
+
}
|
|
341
345
|
function splitFields(data) {
|
|
342
346
|
const archetypeData = {};
|
|
343
347
|
const projectData = {};
|
|
344
348
|
for (const [key, value] of Object.entries(data)) {
|
|
345
|
-
if (["id", "createdAt", "updatedAt", "createdBy", "tenantId", fkColumn].includes(key)) continue;
|
|
349
|
+
if (["id", "createdAt", "updatedAt", "createdBy", "ownerId", "tenantId", fkColumn].includes(key)) continue;
|
|
346
350
|
const snakeKey = camelToSnake(key);
|
|
347
351
|
if (archetypeColumns.has(snakeKey)) {
|
|
348
352
|
archetypeData[snakeKey] = value;
|
|
@@ -364,6 +368,7 @@ function createArchetypeProvider(config) {
|
|
|
364
368
|
if (!tenantId) return { data: [], total: 0 };
|
|
365
369
|
const countOpts = query.countMode === "none" ? void 0 : { count: "exact" };
|
|
366
370
|
let q = isPure ? coreClient().from(ac.table).select("*", countOpts).eq("tenant_id", tenantId) : getClient2().from(viewName).select("*", countOpts).eq("tenant_id", tenantId);
|
|
371
|
+
q = applyUnitScope(q, unitScope());
|
|
367
372
|
if (isPure && HAS_KIND.has(config.archetype)) {
|
|
368
373
|
q = q.eq("kind", config.archetypeKind);
|
|
369
374
|
}
|
|
@@ -413,6 +418,10 @@ function createArchetypeProvider(config) {
|
|
|
413
418
|
if (HAS_KIND.has(config.archetype)) {
|
|
414
419
|
archetypeData.kind = config.archetypeKind;
|
|
415
420
|
}
|
|
421
|
+
const scope = unitScope();
|
|
422
|
+
if (scope && archetypeData[scope.column] === void 0) {
|
|
423
|
+
archetypeData[scope.column] = scope.id;
|
|
424
|
+
}
|
|
416
425
|
const { data: archetypeRow, error: archetypeError } = await coreClient().from(ac.table).insert(archetypeData).select().single();
|
|
417
426
|
if (archetypeError) throw archetypeError;
|
|
418
427
|
const archetypeId = archetypeRow.id;
|
|
@@ -465,8 +474,9 @@ function createArchetypeProvider(config) {
|
|
|
465
474
|
function cachePrefixFor(tenantId, table) {
|
|
466
475
|
return `${tenantId ?? "_"}:${table}`;
|
|
467
476
|
}
|
|
468
|
-
function cacheKeyFor(tenantId, table, query) {
|
|
469
|
-
|
|
477
|
+
function cacheKeyFor(tenantId, table, query, unitId) {
|
|
478
|
+
const keyed = unitId ? { ...query, __unit: unitId } : query;
|
|
479
|
+
return `${cachePrefixFor(tenantId, table)}:${stableKey(keyed)}`;
|
|
470
480
|
}
|
|
471
481
|
function withCache(provider, options) {
|
|
472
482
|
function prefix() {
|
|
@@ -474,7 +484,7 @@ function withCache(provider, options) {
|
|
|
474
484
|
}
|
|
475
485
|
return {
|
|
476
486
|
async list(query) {
|
|
477
|
-
const key = cacheKeyFor(options.tenantId(), options.table, query);
|
|
487
|
+
const key = cacheKeyFor(options.tenantId(), options.table, query, options.unitId?.());
|
|
478
488
|
const cached = globalCache.get(key);
|
|
479
489
|
if (cached) return { ...cached, cached: true };
|
|
480
490
|
const result = await provider.list(query);
|
|
@@ -1034,8 +1044,8 @@ var REPLICA_MAX_ROWS = 5e3;
|
|
|
1034
1044
|
var REPLICA_FIRST_DELAY_MS = 1500;
|
|
1035
1045
|
var REPLICA_REFRESH_DELAY_MS = 3e3;
|
|
1036
1046
|
var REPLICA_MIN_INTERVAL_MS = 6e4;
|
|
1037
|
-
function replicaKeyFor(tenantId, table) {
|
|
1038
|
-
return `replica:${cachePrefixFor(tenantId, table)}`;
|
|
1047
|
+
function replicaKeyFor(tenantId, table, unitId) {
|
|
1048
|
+
return `replica:${cachePrefixFor(tenantId, table)}${unitId ? `@${unitId}` : ""}`;
|
|
1039
1049
|
}
|
|
1040
1050
|
function withOffline(provider, options) {
|
|
1041
1051
|
if (options.policy === "online") return provider;
|
|
@@ -1044,14 +1054,14 @@ function withOffline(provider, options) {
|
|
|
1044
1054
|
async function fromPersisted(query) {
|
|
1045
1055
|
const l2 = getPersistentCache();
|
|
1046
1056
|
if (!l2?.available) return void 0;
|
|
1047
|
-
const entry = await l2.getStale(cacheKeyFor(options.tenantId(), options.table, query));
|
|
1057
|
+
const entry = await l2.getStale(cacheKeyFor(options.tenantId(), options.table, query, options.unitId?.()));
|
|
1048
1058
|
if (!entry) return void 0;
|
|
1049
1059
|
return { ...entry.data, stale: true, staleAt: entry.storedAt };
|
|
1050
1060
|
}
|
|
1051
1061
|
async function fromReplica(query) {
|
|
1052
1062
|
const l2 = getPersistentCache();
|
|
1053
1063
|
if (!l2?.available) return void 0;
|
|
1054
|
-
const snapshot = await l2.getReplica(replicaKeyFor(options.tenantId(), options.table));
|
|
1064
|
+
const snapshot = await l2.getReplica(replicaKeyFor(options.tenantId(), options.table, options.unitId?.()));
|
|
1055
1065
|
if (!snapshot) return void 0;
|
|
1056
1066
|
const result = await localList(snapshot.rows, query, options.entityDef);
|
|
1057
1067
|
return { ...result, stale: true, staleAt: snapshot.syncedAt };
|
|
@@ -1062,7 +1072,7 @@ function withOffline(provider, options) {
|
|
|
1062
1072
|
const result = await provider.list(query);
|
|
1063
1073
|
const l2 = getPersistentCache();
|
|
1064
1074
|
if (options.policy === "cache" && l2?.available) {
|
|
1065
|
-
l2.put(cacheKeyFor(options.tenantId(), options.table, query), result, Date.now() + ttl);
|
|
1075
|
+
l2.put(cacheKeyFor(options.tenantId(), options.table, query, options.unitId?.()), result, Date.now() + ttl);
|
|
1066
1076
|
}
|
|
1067
1077
|
return result;
|
|
1068
1078
|
} catch (err) {
|
|
@@ -1118,13 +1128,13 @@ async function syncReplica(provider, options) {
|
|
|
1118
1128
|
if (result.data.length < pageSize) break;
|
|
1119
1129
|
}
|
|
1120
1130
|
const snapshot = rows.slice(0, maxRows);
|
|
1121
|
-
l2.putReplica(replicaKeyFor(options.tenantId, options.table), snapshot);
|
|
1131
|
+
l2.putReplica(replicaKeyFor(options.tenantId, options.table, options.unitId), snapshot);
|
|
1122
1132
|
await l2.flush();
|
|
1123
1133
|
return snapshot.length;
|
|
1124
1134
|
}
|
|
1125
1135
|
var replicaSyncs = /* @__PURE__ */ new Map();
|
|
1126
1136
|
function ensureReplicaSync(provider, options) {
|
|
1127
|
-
const key = replicaKeyFor(options.tenantId(), options.table);
|
|
1137
|
+
const key = replicaKeyFor(options.tenantId(), options.table, options.unitId?.());
|
|
1128
1138
|
if (replicaSyncs.has(key)) return;
|
|
1129
1139
|
const state = {
|
|
1130
1140
|
// Tenant read at sync time, not at registration: the first provider is often
|
|
@@ -1132,6 +1142,7 @@ function ensureReplicaSync(provider, options) {
|
|
|
1132
1142
|
run: () => syncReplica(provider, {
|
|
1133
1143
|
table: options.table,
|
|
1134
1144
|
tenantId: options.tenantId(),
|
|
1145
|
+
unitId: options.unitId?.(),
|
|
1135
1146
|
pageSize: options.replicaPageSize,
|
|
1136
1147
|
maxRows: options.replicaMaxRows
|
|
1137
1148
|
}),
|
|
@@ -1149,7 +1160,7 @@ function ensureReplicaSync(provider, options) {
|
|
|
1149
1160
|
scheduleReplicaSync(options, REPLICA_FIRST_DELAY_MS);
|
|
1150
1161
|
}
|
|
1151
1162
|
function scheduleReplicaSync(options, delayMs) {
|
|
1152
|
-
const state = replicaSyncs.get(replicaKeyFor(options.tenantId(), options.table));
|
|
1163
|
+
const state = replicaSyncs.get(replicaKeyFor(options.tenantId(), options.table, options.unitId?.()));
|
|
1153
1164
|
if (!state || state.timer) return;
|
|
1154
1165
|
state.timer = setTimeout(() => {
|
|
1155
1166
|
state.timer = null;
|
|
@@ -1247,6 +1258,47 @@ function mergeCustomFields(current, patch) {
|
|
|
1247
1258
|
}
|
|
1248
1259
|
return merged;
|
|
1249
1260
|
}
|
|
1261
|
+
var _activeUnitId;
|
|
1262
|
+
var _unitMode = false;
|
|
1263
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1264
|
+
function notify() {
|
|
1265
|
+
clearGlobalCache();
|
|
1266
|
+
for (const l of listeners) l();
|
|
1267
|
+
}
|
|
1268
|
+
function setActiveUnitId(id) {
|
|
1269
|
+
if (id === _activeUnitId) return;
|
|
1270
|
+
_activeUnitId = id;
|
|
1271
|
+
notify();
|
|
1272
|
+
}
|
|
1273
|
+
function getActiveUnitId() {
|
|
1274
|
+
return _unitMode ? _activeUnitId : void 0;
|
|
1275
|
+
}
|
|
1276
|
+
function setUnitMode(on) {
|
|
1277
|
+
if (on === _unitMode) return;
|
|
1278
|
+
_unitMode = on;
|
|
1279
|
+
notify();
|
|
1280
|
+
}
|
|
1281
|
+
function isUnitMode() {
|
|
1282
|
+
return _unitMode;
|
|
1283
|
+
}
|
|
1284
|
+
function subscribeActiveUnit(listener) {
|
|
1285
|
+
listeners.add(listener);
|
|
1286
|
+
return () => {
|
|
1287
|
+
listeners.delete(listener);
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
function useActiveUnitId() {
|
|
1291
|
+
return React.useSyncExternalStore(subscribeActiveUnit, getActiveUnitId, getActiveUnitId);
|
|
1292
|
+
}
|
|
1293
|
+
function useUnitMode() {
|
|
1294
|
+
return React.useSyncExternalStore(subscribeActiveUnit, isUnitMode, isUnitMode);
|
|
1295
|
+
}
|
|
1296
|
+
function resetActiveUnit() {
|
|
1297
|
+
if (_activeUnitId === void 0 && !_unitMode) return;
|
|
1298
|
+
_activeUnitId = void 0;
|
|
1299
|
+
_unitMode = false;
|
|
1300
|
+
notify();
|
|
1301
|
+
}
|
|
1250
1302
|
|
|
1251
1303
|
// src/data/custom-fields.ts
|
|
1252
1304
|
var KEY = "customFields";
|
|
@@ -1311,10 +1363,20 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
|
|
|
1311
1363
|
const client = getSupabaseClientOptional();
|
|
1312
1364
|
if (client && entityDef.data?.table) {
|
|
1313
1365
|
const tenantId = () => getActiveTenantId();
|
|
1366
|
+
const registered = getScopedResource(entityDef.data.table);
|
|
1367
|
+
const unitScoped = entityDef.data.unitScoped ?? Boolean(registered);
|
|
1368
|
+
const unitId = unitScoped ? () => getActiveUnitId() : void 0;
|
|
1369
|
+
const unitConfig = {
|
|
1370
|
+
unitScoped,
|
|
1371
|
+
unitColumn: entityDef.data.unitIdColumn ?? registered?.unitColumn,
|
|
1372
|
+
includeSharedRows: entityDef.data.includeSharedRows,
|
|
1373
|
+
unitId
|
|
1374
|
+
};
|
|
1314
1375
|
const cacheOptions = {
|
|
1315
1376
|
table: entityDef.data.archetypeKind ? `${entityDef.data.table}#${entityDef.data.archetypeKind}` : entityDef.data.table,
|
|
1316
1377
|
tenantId,
|
|
1317
|
-
ttl: entityDef.data.cacheTTL
|
|
1378
|
+
ttl: entityDef.data.cacheTTL,
|
|
1379
|
+
unitId
|
|
1318
1380
|
};
|
|
1319
1381
|
const offlinePolicy = entityDef.data.offline ?? "online";
|
|
1320
1382
|
const cached = (p) => {
|
|
@@ -1327,7 +1389,8 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
|
|
|
1327
1389
|
archetypeKind: entityDef.data.archetypeKind,
|
|
1328
1390
|
projectTable: entityDef.data.table,
|
|
1329
1391
|
tenantId,
|
|
1330
|
-
searchColumns: entityDef.data.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key)
|
|
1392
|
+
searchColumns: entityDef.data.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key),
|
|
1393
|
+
...unitConfig
|
|
1331
1394
|
}), entityDef.data.archetype));
|
|
1332
1395
|
}
|
|
1333
1396
|
return cached(createSupabaseProvider(entityDef.data.table, {
|
|
@@ -1338,7 +1401,8 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
|
|
|
1338
1401
|
selectColumns: entityDef.data.selectColumns,
|
|
1339
1402
|
columnMap: entityDef.data.columnMap,
|
|
1340
1403
|
filters: entityDef.data.filters,
|
|
1341
|
-
defaults: entityDef.data.defaults
|
|
1404
|
+
defaults: entityDef.data.defaults,
|
|
1405
|
+
...unitConfig
|
|
1342
1406
|
}));
|
|
1343
1407
|
}
|
|
1344
1408
|
if (entityDef.data?.table) {
|
|
@@ -1349,6 +1413,21 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
|
|
|
1349
1413
|
return createMockProvider(entityDef, mockData);
|
|
1350
1414
|
}
|
|
1351
1415
|
|
|
1416
|
+
// src/data/missing-objects.ts
|
|
1417
|
+
var missing = /* @__PURE__ */ new Set();
|
|
1418
|
+
function isKnownMissing(key) {
|
|
1419
|
+
return missing.has(key);
|
|
1420
|
+
}
|
|
1421
|
+
function rememberIfMissing(key, error) {
|
|
1422
|
+
if (!error) return false;
|
|
1423
|
+
if (toDataError(error).kind !== "missing-object") return false;
|
|
1424
|
+
missing.add(key);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
function resetMissingObjects() {
|
|
1428
|
+
missing.clear();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1352
1431
|
// src/data/query.ts
|
|
1353
1432
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
1354
1433
|
var DEFAULT_RETRY_DELAY_MS = 150;
|
|
@@ -1613,6 +1692,6 @@ async function bulkRemove(provider, ids, options) {
|
|
|
1613
1692
|
return { removed, failed };
|
|
1614
1693
|
}
|
|
1615
1694
|
|
|
1616
|
-
export { CustomFieldsError, DATA_CHANGED_EVENT, DataError, FIELDS_TABLE, attachPersistentCache, bulkCreate, bulkRemove, cacheKeyFor, cachePrefixFor, clearCustomFieldsCache, countByTenant, createArchetypeProvider, createEventBus, createFayzApiProvider, createMockProvider, dataCall, dataQuery, emitDataChanged, eventBus, getPersistentCache, invalidateCount, localList, matchesDataChange, mergeCustomFields, registerDataTelemetry, replicaKeyFor, resolveCustomFields, resolveDataProvider, syncReplica, toDataError, useDataChanged, validateCustomFields, withCache, withOffline };
|
|
1617
|
-
//# sourceMappingURL=chunk-
|
|
1618
|
-
//# sourceMappingURL=chunk-
|
|
1695
|
+
export { CustomFieldsError, DATA_CHANGED_EVENT, DataError, FIELDS_TABLE, attachPersistentCache, bulkCreate, bulkRemove, cacheKeyFor, cachePrefixFor, clearCustomFieldsCache, countByTenant, createArchetypeProvider, createEventBus, createFayzApiProvider, createMockProvider, dataCall, dataQuery, emitDataChanged, eventBus, getActiveUnitId, getPersistentCache, invalidateCount, isKnownMissing, isUnitMode, localList, matchesDataChange, mergeCustomFields, registerDataTelemetry, rememberIfMissing, replicaKeyFor, resetActiveUnit, resetMissingObjects, resolveCustomFields, resolveDataProvider, setActiveUnitId, setUnitMode, subscribeActiveUnit, syncReplica, toDataError, useActiveUnitId, useDataChanged, useUnitMode, validateCustomFields, withCache, withOffline };
|
|
1696
|
+
//# sourceMappingURL=chunk-2ZAUJVYP.js.map
|
|
1697
|
+
//# sourceMappingURL=chunk-2ZAUJVYP.js.map
|