{
  "version": 3,
  "sources": ["../../../../node_modules/@sniptt/guards/lib/guards/primitives.ts", "../../../../node_modules/@sniptt/guards/lib/guards/structural.ts", "../../../../node_modules/@sniptt/guards/lib/guards/convenience.ts", "../../../../node_modules/@sniptt/guards/lib/index.ts", "twenty-sdk-define-stub:__twenty-sdk-define-stub__", "../../../../node_modules/twenty-shared/dist/is-record-object-schema-CwzshFdt.mjs", "../../../../node_modules/twenty-shared/dist/logic-function.mjs", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/errors/app-connection-auth-failed.error.ts", "../../../../node_modules/twenty-shared/dist/FieldMetadataType-PppCGM82.mjs", "../../../../node_modules/twenty-shared/dist/get-system-view-universal-identifier.util-CJoglbKX.mjs", "../../../../node_modules/twenty-shared/dist/application.mjs", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/utils/post-graphql-request.util.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/get-connection.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/list-connections.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/find-connection-for-request.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/agents/run-agent.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/jobs/enqueue-job.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/key-value/kv.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/response.ts", "../../../../src/constants/licence-identifiers.ts", "../../../../src/licensing/audit.ts", "../../../../src/licensing/feature-gate.ts", "../../../../src/licensing/mask.ts", "../../../../src/licensing/panel.ts", "../../../../src/logic-functions/greenlight-api.ts", "../../../../src/logic-functions/licence-cache-store.ts", "../../../../src/licensing/cache.ts", "../../../../src/calibration/state.ts", "../../../../src/enrichment/field-specs.ts", "../../../../src/logic-functions/licence-run.ts", "../../../../src/logic-functions/licence-status.logic-function.ts"],
  "sourcesContent": [null, null, null, null, "\n// Auto-generated stub for twenty-sdk/define injected by the SDK CLI build.\n// Real implementations would pull in zod, twenty-shared and ~1MB of code; at\n// runtime only `default.config.handler` is consumed, so tiny no-ops suffice.\nconst __defineFactoryStub = (config) => ({\n  success: true,\n  config,\n  errors: [],\n});\n\nconst __anyHandler = {\n  get(_target, prop) {\n    if (prop === '__esModule') return true;\n    if (prop === Symbol.toPrimitive) return () => '';\n    if (typeof prop === 'symbol') return undefined;\n    return new Proxy(() => undefined, __anyHandler);\n  },\n  apply() {\n    return new Proxy(() => undefined, __anyHandler);\n  },\n};\nconst __anyStub = new Proxy(() => undefined, __anyHandler);\n\nexport const createValidationResult = __defineFactoryStub;\nexport const defineAgent = __defineFactoryStub;\nexport const defineApplication = __defineFactoryStub;\nexport const defineApplicationRole = __defineFactoryStub;\nexport const defineCommandMenuItem = __defineFactoryStub;\nexport const defineConnectionProvider = __defineFactoryStub;\nexport const defineField = __defineFactoryStub;\nexport const defineFrontComponent = __defineFactoryStub;\nexport const defineIndex = __defineFactoryStub;\nexport const defineLogicFunction = __defineFactoryStub;\nexport const defineNavigationMenuItem = __defineFactoryStub;\nexport const defineObject = __defineFactoryStub;\nexport const definePageLayout = __defineFactoryStub;\nexport const definePageLayoutTab = __defineFactoryStub;\nexport const definePermissionFlag = __defineFactoryStub;\nexport const definePostInstallLogicFunction = __defineFactoryStub;\nexport const definePreInstallLogicFunction = __defineFactoryStub;\nexport const defineRole = __defineFactoryStub;\nexport const defineSettingsFrontComponent = __defineFactoryStub;\nexport const defineSkill = __defineFactoryStub;\nexport const defineUninstallLogicFunction = __defineFactoryStub;\nexport const defineView = __defineFactoryStub;\nexport const defineViewField = __defineFactoryStub;\nexport const AggregateOperations = __anyStub;\nexport const DateDisplayFormat = __anyStub;\nexport const FieldMetadataSettingsOnClickAction = __anyStub;\nexport const FieldType = __anyStub;\nexport const HTTPMethod = __anyStub;\nexport const NavigationMenuItemType = __anyStub;\nexport const NumberDataType = __anyStub;\nexport const ObjectRecordGroupByDateGranularity = __anyStub;\nexport const OnDeleteAction = __anyStub;\nexport const PageLayoutTabLayoutMode = __anyStub;\nexport const PageLayoutType = __anyStub;\nexport const RelationType = __anyStub;\nexport const RowLevelPermissionPredicateGroupLogicalOperator = __anyStub;\nexport const RowLevelPermissionPredicateOperand = __anyStub;\nexport const STANDARD_OBJECT = __anyStub;\nexport const STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = __anyStub;\nexport const STANDARD_PAGE_LAYOUT = __anyStub;\nexport const STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS = __anyStub;\nexport const SystemPermissionFlag = __anyStub;\nexport const ViewCalendarLayout = __anyStub;\nexport const ViewFilterGroupLogicalOperator = __anyStub;\nexport const ViewFilterOperand = __anyStub;\nexport const ViewKey = __anyStub;\nexport const ViewOpenRecordIn = __anyStub;\nexport const ViewSortDirection = __anyStub;\nexport const ViewType = __anyStub;\nexport const ViewVisibility = __anyStub;\nexport const canAccessFullAdminPanel = __anyStub;\nexport const canImpersonate = __anyStub;\nexport const every = __anyStub;\nexport const everyDefined = __anyStub;\nexport const everyEquals = __anyStub;\nexport const favoriteRecordIds = __anyStub;\nexport const featureFlags = __anyStub;\nexport const getFieldUniversalIdentifier = __anyStub;\nexport const getSystemRelationFieldUniversalIdentifier = __anyStub;\nexport const getSystemViewFieldUniversalIdentifier = __anyStub;\nexport const getSystemViewUniversalIdentifier = __anyStub;\nexport const hasAnySoftDeleteFilterOnView = __anyStub;\nexport const includes = __anyStub;\nexport const includesEvery = __anyStub;\nexport const isDashboardPageLayoutInEditMode = __anyStub;\nexport const isDefined = __anyStub;\nexport const isInSidePanel = __anyStub;\nexport const isLayoutCustomizationModeEnabled = __anyStub;\nexport const isNonEmptyString = __anyStub;\nexport const isSelectAll = __anyStub;\nexport const none = __anyStub;\nexport const noneDefined = __anyStub;\nexport const noneEquals = __anyStub;\nexport const numberOfSelectedRecords = __anyStub;\nexport const objectMetadataItem = __anyStub;\nexport const objectMetadataLabel = __anyStub;\nexport const objectPermissions = __anyStub;\nexport const pageType = __anyStub;\nexport const selectedRecords = __anyStub;\nexport const some = __anyStub;\nexport const someDefined = __anyStub;\nexport const someEquals = __anyStub;\nexport const someNonEmptyString = __anyStub;\nexport const targetObjectReadPermissions = __anyStub;\nexport const targetObjectWritePermissions = __anyStub;\nexport const validateFields = __anyStub;\n", "import { isNonEmptyString as e } from \"@sniptt/guards\";\n//#region src/logic-function/is-record-object-schema.ts\nvar t = (t) => (t?.type === \"record\" || t?.type === \"object\") && e(t.objectUniversalIdentifier);\n//#endregion\nexport { t };\n", "import { t as e } from \"./isDefined-Dtu5EYqP.mjs\";\nimport { t } from \"./is-record-object-schema-CwzshFdt.mjs\";\nimport { isNonEmptyString as n, isObject as r } from \"@sniptt/guards\";\n//#region src/logic-function/build-tool-input-json-schema.ts\nvar i = (e, t) => {\n\tlet r = n(e) ? t?.(e) : void 0;\n\treturn `Id of the ${n(r) ? r : \"linked\"} record`;\n}, a = (n, o) => {\n\tif (t(n)) return {\n\t\ttype: \"string\",\n\t\tdescription: i(n.objectUniversalIdentifier, o)\n\t};\n\tif (n.type === \"records\") return {\n\t\ttype: \"array\",\n\t\titems: {\n\t\t\ttype: \"string\",\n\t\t\tdescription: i(n.objectUniversalIdentifier, o)\n\t\t}\n\t};\n\tlet { objectUniversalIdentifier: s, multiline: c, label: l, items: u, properties: d, additionalProperties: f, ...p } = n, m = { ...p };\n\treturn e(u) && (m.items = a(u, o)), e(d) && (m.properties = Object.fromEntries(Object.entries(d).map(([e, t]) => [e, a(t, o)]))), e(f) && (m.additionalProperties = r(f) ? a(f, o) : f), m;\n}, o = {\n\ttype: \"object\",\n\tproperties: {}\n}, s = (e) => {\n\tlet t = { type: \"unknown\" };\n\tswitch (e.type) {\n\t\tcase \"string\":\n\t\t\tt.type = \"string\";\n\t\t\tbreak;\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\tt.type = \"number\";\n\t\t\tbreak;\n\t\tcase \"boolean\":\n\t\t\tt.type = \"boolean\";\n\t\t\tbreak;\n\t\tcase \"array\":\n\t\t\tt.type = \"array\", e.items && (t.items = s(e.items));\n\t\t\tbreak;\n\t\tcase \"object\":\n\t\t\tt.type = \"object\", e.properties && (t.properties = Object.fromEntries(Object.entries(e.properties).map(([e, t]) => [e, s(t)])));\n\t\t\tbreak;\n\t\tcase \"record\":\n\t\t\tt.type = \"record\";\n\t\t\tbreak;\n\t\tcase \"records\":\n\t\t\tt.type = \"records\";\n\t\t\tbreak;\n\t\tdefault: t.type = \"unknown\";\n\t}\n\treturn Array.isArray(e.enum) && (t.enum = e.enum.filter((e) => typeof e == \"string\")), e.multiline === !0 && (t.multiline = !0), n(e.label) && (t.label = e.label), n(e.objectUniversalIdentifier) && (t.objectUniversalIdentifier = e.objectUniversalIdentifier), t;\n}, c = (e) => [s(e)], l = { inputSchema: c({\n\ttype: \"object\",\n\tproperties: {\n\t\ta: { type: \"string\" },\n\t\tb: { type: \"number\" }\n\t}\n}) }, u = async (t) => {\n\tlet { getFunctionInputSchema: n } = await import(\"./get-function-input-schema-GNk3NRLJ.mjs\"), r = n(t)[0];\n\treturn r?.type === \"object\" && e(r.properties) ? {\n\t\ttype: \"object\",\n\t\tproperties: r.properties\n\t} : o;\n}, d = (t) => !e(t) || t === null ? \"unknown\" : typeof t == \"string\" ? \"string\" : typeof t == \"number\" ? \"number\" : typeof t == \"boolean\" ? \"boolean\" : Array.isArray(t) ? \"array\" : \"unknown\", f = (e) => e ? Object.entries(e).reduce((e, [t, n]) => (r(n) && !Array.isArray(n) ? e[t] = {\n\tisLeaf: !1,\n\ttype: \"object\",\n\tlabel: t,\n\tvalue: f(n)\n} : e[t] = {\n\tisLeaf: !0,\n\tvalue: n,\n\ttype: d(n),\n\tlabel: t\n}, e), {}) : {}, p = (e, t) => e ? `${e}.${t}` : t, m = (t, n, r = \"\") => {\n\tlet i = [];\n\tfor (let [a, o] of Object.entries(n)) {\n\t\tlet n = p(r, a), s = t[a];\n\t\tif (!e(s)) {\n\t\t\ti.push(`Missing key \"${n}\" in declared output schema.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (o.isLeaf !== s.isLeaf) {\n\t\t\ti.push(`Type mismatch at \"${n}\": expected ${o.isLeaf ? o.type : \"object\"} but declared ${s.isLeaf ? s.type : \"object\"}.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!o.isLeaf && !s.isLeaf) {\n\t\t\ti.push(...m(s.value, o.value, n));\n\t\t\tcontinue;\n\t\t}\n\t\to.isLeaf && s.isLeaf && o.type !== \"unknown\" && s.type !== \"unknown\" && o.type !== s.type && i.push(`Type mismatch at \"${n}\": expected ${o.type} but declared ${s.type}.`);\n\t}\n\treturn i;\n}, h = [\n\t\"string\",\n\t\"number\",\n\t\"boolean\",\n\t\"array\",\n\t\"unknown\"\n], g = (e) => h.includes(e), _ = (e, t) => {\n\tlet n = t.label ?? e;\n\treturn t.type === \"record\" ? {\n\t\tisLeaf: !0,\n\t\ttype: \"string\",\n\t\tlabel: n,\n\t\tvalue: null\n\t} : t.type === \"records\" ? {\n\t\tisLeaf: !0,\n\t\ttype: \"array\",\n\t\tlabel: n,\n\t\tvalue: null\n\t} : t.type === \"object\" ? {\n\t\tisLeaf: !1,\n\t\ttype: \"object\",\n\t\tlabel: n,\n\t\tvalue: r(t.properties) ? v(t.properties) : {}\n\t} : {\n\t\tisLeaf: !0,\n\t\ttype: g(t.type) ? t.type : \"unknown\",\n\t\tlabel: n,\n\t\tvalue: null\n\t};\n}, v = (e) => Object.entries(e).reduce((e, [t, n]) => (e[t] = _(t, n), e), {}), y = (e) => {\n\tlet t = e[0];\n\treturn t?.type !== \"object\" || !r(t.properties) ? {} : v(t.properties);\n}, b = (e) => e?.type === \"records\" && n(e.objectUniversalIdentifier) || e?.type === \"array\" && t(e?.items);\n//#endregion\nexport { o as DEFAULT_TOOL_INPUT_SCHEMA, l as SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS, a as buildToolInputJsonSchema, u as getInputSchemaFromSourceCode, f as getOutputSchemaFromValue, m as getOutputSchemaMismatchIssues, y as inputSchemaToOutputSchema, b as isRecordArraySchema, t as isRecordObjectSchema, c as jsonSchemaToInputSchema };\n", "// Thrown when the platform asks the SDK to operate on a connection whose\n// OAuth refresh failed permanently (`authFailedAt` is set). The end user\n// must reconnect from the app's settings tab — the app cannot recover on\n// its own.\n//\n// `listConnections` filters these out by default (the user can't act on\n// them anyway). `getConnection` throws this when the looked-up connection\n// is in this state, so a stored connection id can be safely retried until\n// it works again.\nexport class AppConnectionAuthFailedError extends Error {\n  readonly connectionId: string;\n\n  constructor(connectionId: string) {\n    super(\n      `App connection ${connectionId} requires the user to reconnect ` +\n        `(authFailedAt is set). Surface a \"Reconnect\" prompt in your UI.`,\n    );\n    this.name = 'AppConnectionAuthFailedError';\n    this.connectionId = connectionId;\n  }\n}\n", "//#region src/types/FieldMetadataType.ts\nvar e = /* @__PURE__ */ function(e) {\n\treturn e.ACTOR = \"ACTOR\", e.ADDRESS = \"ADDRESS\", e.ARRAY = \"ARRAY\", e.BOOLEAN = \"BOOLEAN\", e.CURRENCY = \"CURRENCY\", e.DATE = \"DATE\", e.DATE_TIME = \"DATE_TIME\", e.EMAILS = \"EMAILS\", e.FILES = \"FILES\", e.FULL_NAME = \"FULL_NAME\", e.LINKS = \"LINKS\", e.MORPH_RELATION = \"MORPH_RELATION\", e.MULTI_SELECT = \"MULTI_SELECT\", e.NUMBER = \"NUMBER\", e.NUMERIC = \"NUMERIC\", e.PHONES = \"PHONES\", e.POSITION = \"POSITION\", e.RATING = \"RATING\", e.RAW_JSON = \"RAW_JSON\", e.RELATION = \"RELATION\", e.RICH_TEXT = \"RICH_TEXT\", e.SELECT = \"SELECT\", e.TEXT = \"TEXT\", e.TS_VECTOR = \"TS_VECTOR\", e.UUID = \"UUID\", e;\n}({});\n//#endregion\nexport { e as t };\n", "import { v5 as e } from \"uuid\";\n//#region src/application/constants/TwentyStandardApplicationUniversalIdentifier.ts\nvar t = \"20202020-64aa-4b6f-b003-9c74b97cee20\", n = ({ entityNamespace: t, value: n, applicationUniversalIdentifier: r }) => e(`${t}:${n}`, r), r = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: t, name: r }) => n({\n\tentityNamespace: \"fieldMetadata\",\n\tvalue: `${t}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), i = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: t, relationTargetObjectUniversalIdentifier: r }) => n({\n\tentityNamespace: \"fieldMetadata\",\n\tvalue: `${t}:systemRelation:${r}`,\n\tapplicationUniversalIdentifier: e\n}), a = ({ fieldMetadataApplicationUniversalIdentifier: e, viewUniversalIdentifier: t, fieldMetadataUniversalIdentifier: r }) => n({\n\tentityNamespace: \"viewField\",\n\tvalue: `${t}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), o = ({ objectMetadataApplicationUniversalIdentifier: e, objectUniversalIdentifier: t, viewKey: r }) => n({\n\tentityNamespace: \"view\",\n\tvalue: `${t}:${r}`,\n\tapplicationUniversalIdentifier: e\n});\n//#endregion\nexport { n as a, r as i, a as n, t as o, i as r, o as t };\n", "import { t as e } from \"./FieldMetadataType-PppCGM82.mjs\";\nimport { a as t, i as n, n as r, o as i, r as a, t as o } from \"./get-system-view-universal-identifier.util-CJoglbKX.mjs\";\n//#region src/application/applicationCategoryType.ts\nvar s = [\n\t\"Communication\",\n\t\"Productivity\",\n\t\"Product management\",\n\t\"Sales\",\n\t\"Marketing\",\n\t\"Enrichment\",\n\t\"Data\",\n\t\"Search\",\n\t\"Other\"\n], c = (e) => s.includes(e), l = [\n\te.TEXT,\n\te.ARRAY,\n\te.BOOLEAN,\n\te.DATE,\n\te.DATE_TIME,\n\te.NUMBER,\n\te.NUMERIC,\n\te.RAW_JSON,\n\te.RICH_TEXT,\n\te.SELECT,\n\te.MULTI_SELECT\n], u = \"public\", d = \"TWENTY_API_KEY\", f = \"TWENTY_API_URL\", p = \"TWENTY_APP_ACCESS_TOKEN\", m = \"TWENTY_FUNCTIONS_URL\", h = \"generated\", g = { js: \"import { createRequire as __createRequire } from 'module';\\nconst require = __createRequire(import.meta.url);\" }, _ = \".twenty/output\", v = \"Standard\", y = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"agent\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), b = ({ applicationUniversalIdentifier: e, key: n }) => t({\n\tentityNamespace: \"applicationVariable\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), x = \"navigation\", S = ({ applicationUniversalIdentifier: e, engineComponentKey: n }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `GLOBAL:${n}`,\n\tapplicationUniversalIdentifier: e\n}), C = ({ applicationUniversalIdentifier: e, engineComponentKey: n }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `GLOBAL_OBJECT_CONTEXT:${n}`,\n\tapplicationUniversalIdentifier: e\n}), w = ({ applicationUniversalIdentifier: e, engineComponentKey: n, objectUniversalIdentifier: r }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `RECORD_SELECTION:${n}:${r ?? \"\"}`,\n\tapplicationUniversalIdentifier: e\n}), T = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `${n}:${x}`,\n\tapplicationUniversalIdentifier: e\n}), E = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"connectionProvider\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), D = ({ applicationUniversalIdentifier: e, roleUniversalIdentifier: n, fieldUniversalIdentifier: r }) => t({\n\tentityNamespace: \"fieldPermission\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), O = ({ applicationUniversalIdentifier: e, pageLayoutWidgetUniversalIdentifier: n }) => t({\n\tentityNamespace: \"view\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), k = ({ applicationUniversalIdentifier: e, componentName: n }) => t({\n\tentityNamespace: \"frontComponent\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), A = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"index\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), j = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"logicFunction\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), ee = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `FOLDER:${n}`,\n\tapplicationUniversalIdentifier: e\n}), M = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `OBJECT:${n}`,\n\tapplicationUniversalIdentifier: e\n}), N = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `VIEW:${n}`,\n\tapplicationUniversalIdentifier: e\n}), P = ({ applicationUniversalIdentifier: e, link: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `LINK:${n}`,\n\tapplicationUniversalIdentifier: e\n}), F = ({ applicationUniversalIdentifier: e, roleUniversalIdentifier: n, objectUniversalIdentifier: r }) => t({\n\tentityNamespace: \"objectPermission\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), I = ({ applicationUniversalIdentifier: e, nameSingular: n }) => t({\n\tentityNamespace: \"objectMetadata\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), L = ({ applicationUniversalIdentifier: e, pageLayoutUniversalIdentifier: n, title: r }) => t({\n\tentityNamespace: \"pageLayoutTab\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), R = \"RECORD_PAGE\", z = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"pageLayout\",\n\tvalue: n ? `${n}:${r}` : r,\n\tapplicationUniversalIdentifier: e\n}), B = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n }) => t({\n\tentityNamespace: \"pageLayout\",\n\tvalue: `${n}:${R}`,\n\tapplicationUniversalIdentifier: e\n}), V = ({ applicationUniversalIdentifier: e, pageLayoutTabUniversalIdentifier: n, title: r }) => t({\n\tentityNamespace: \"pageLayoutWidget\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), H = ({ applicationUniversalIdentifier: e, key: n }) => t({\n\tentityNamespace: \"permissionFlag\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), U = ({ applicationUniversalIdentifier: e, roleUniversalIdentifier: n, permissionFlagUniversalIdentifier: r }) => t({\n\tentityNamespace: \"rolePermissionFlag\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), W = ({ applicationUniversalIdentifier: e, agentUniversalIdentifier: n }) => t({\n\tentityNamespace: \"roleTarget\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), G = ({ applicationUniversalIdentifier: e, label: n }) => t({\n\tentityNamespace: \"role\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), K = ({ applicationUniversalIdentifier: e, fieldMetadataUniversalIdentifier: n }) => t({\n\tentityNamespace: \"searchFieldMetadata\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), q = ({ applicationUniversalIdentifier: e, fieldUniversalIdentifier: n, value: r }) => t({\n\tentityNamespace: \"selectOption\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), J = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"skill\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), Y = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"viewFieldGroup\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), X = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldMetadataUniversalIdentifier: r }) => t({\n\tentityNamespace: \"viewField\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), Z = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldMetadataUniversalIdentifier: r, operand: i, subFieldName: a }) => t({\n\tentityNamespace: \"viewFilter\",\n\tvalue: `${n}:${r}:${i}:${a ?? \"\"}`,\n\tapplicationUniversalIdentifier: e\n}), Q = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldValue: r }) => t({\n\tentityNamespace: \"viewGroup\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), $ = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldMetadataUniversalIdentifier: r }) => t({\n\tentityNamespace: \"viewSort\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), te = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"view\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), ne = /* @__PURE__ */ function(e) {\n\treturn e.Object = \"object\", e.Field = \"field\", e.LogicFunction = \"logicFunction\", e.FrontComponent = \"frontComponent\", e.Role = \"role\", e.Skill = \"skill\", e.Agent = \"agent\", e.ConnectionProvider = \"connectionProvider\", e.View = \"view\", e.ViewField = \"viewField\", e.NavigationMenuItem = \"navigationMenuItem\", e.PageLayout = \"pageLayout\", e.PageLayoutTab = \"pageLayoutTab\", e.CommandMenuItem = \"commandMenuItem\", e;\n}({}), re = (t, n = e.TEXT) => {\n\tif (t == null) return \"\";\n\tswitch (n) {\n\t\tcase e.BOOLEAN: return String(t) === \"true\" ? \"true\" : \"false\";\n\t\tcase e.NUMBER:\n\t\tcase e.NUMERIC: return String(t);\n\t\tcase e.ARRAY:\n\t\tcase e.MULTI_SELECT:\n\t\t\tif (Array.isArray(t)) return JSON.stringify(t);\n\t\t\tif (typeof t == \"string\") {\n\t\t\t\ttry {\n\t\t\t\t\tlet e = JSON.parse(t);\n\t\t\t\t\tif (Array.isArray(e)) return t;\n\t\t\t\t} catch {}\n\t\t\t\treturn JSON.stringify([t]);\n\t\t\t}\n\t\t\treturn JSON.stringify(t);\n\t\tcase e.RAW_JSON:\n\t\tcase e.RICH_TEXT: return typeof t == \"string\" ? t : JSON.stringify(t);\n\t\tdefault: return typeof t == \"string\" ? t : String(t);\n\t}\n}, ie = (t, n = e.TEXT) => {\n\tif (t === \"\") return n === e.ARRAY || n === e.MULTI_SELECT ? [] : \"\";\n\tswitch (n) {\n\t\tcase e.BOOLEAN: return t === \"true\";\n\t\tcase e.NUMBER:\n\t\tcase e.NUMERIC: {\n\t\t\tlet e = Number(t);\n\t\t\treturn Number.isNaN(e) ? t : e;\n\t\t}\n\t\tcase e.ARRAY:\n\t\tcase e.MULTI_SELECT: try {\n\t\t\tlet e = JSON.parse(t);\n\t\t\treturn Array.isArray(e) ? e : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tcase e.RAW_JSON:\n\t\tcase e.RICH_TEXT: try {\n\t\t\treturn JSON.parse(t);\n\t\t} catch {\n\t\t\treturn t;\n\t\t}\n\t\tdefault: return t;\n\t}\n};\n//#endregion\nexport { s as APPLICATION_CATEGORIES, l as APPLICATION_VARIABLE_FIELD_METADATA_TYPES, u as ASSETS_DIR, d as DEFAULT_API_KEY_NAME, f as DEFAULT_API_URL_NAME, p as DEFAULT_APP_ACCESS_TOKEN_NAME, m as DEFAULT_FUNCTIONS_URL_NAME, h as GENERATED_DIR, g as NODE_ESM_CJS_BANNER, _ as OUTPUT_DIR, ne as SyncableEntity, v as TWENTY_STANDARD_APPLICATION_NAME, i as TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, t as computeDeterministicUuid, ie as deserializeApplicationVariableValue, y as getAgentUniversalIdentifier, b as getApplicationVariableUniversalIdentifier, E as getConnectionProviderUniversalIdentifier, D as getFieldPermissionUniversalIdentifier, n as getFieldUniversalIdentifier, O as getFieldsWidgetViewUniversalIdentifier, ee as getFolderNavigationMenuItemUniversalIdentifier, k as getFrontComponentUniversalIdentifier, S as getGlobalCommandMenuItemUniversalIdentifier, C as getGlobalObjectContextCommandMenuItemUniversalIdentifier, A as getIndexUniversalIdentifier, P as getLinkNavigationMenuItemUniversalIdentifier, j as getLogicFunctionUniversalIdentifier, T as getNavigationCommandUniversalIdentifier, M as getObjectNavigationMenuItemUniversalIdentifier, F as getObjectPermissionUniversalIdentifier, I as getObjectUniversalIdentifier, L as getPageLayoutTabUniversalIdentifier, z as getPageLayoutUniversalIdentifier, V as getPageLayoutWidgetUniversalIdentifier, H as getPermissionFlagUniversalIdentifier, B as getRecordPageLayoutUniversalIdentifier, w as getRecordSelectionCommandMenuItemUniversalIdentifier, U as getRolePermissionFlagUniversalIdentifier, W as getRoleTargetUniversalIdentifier, G as getRoleUniversalIdentifier, K as getSearchFieldUniversalIdentifier, q as getSelectOptionUniversalIdentifier, J as getSkillUniversalIdentifier, a as getSystemRelationFieldUniversalIdentifier, r as getSystemViewFieldUniversalIdentifier, o as getSystemViewUniversalIdentifier, Y as getViewFieldGroupUniversalIdentifier, X as getViewFieldUniversalIdentifier, Z as getViewFilterUniversalIdentifier, Q as getViewGroupUniversalIdentifier, N as getViewNavigationMenuItemUniversalIdentifier, $ as getViewSortUniversalIdentifier, te as getViewUniversalIdentifier, c as isKnownApplicationCategory, re as serializeApplicationVariableValue };\n", "import {\n  DEFAULT_API_URL_NAME,\n  DEFAULT_APP_ACCESS_TOKEN_NAME,\n} from 'twenty-shared/application';\n\nexport const postGraphqlRequest = async <TVariables, TData>({\n  query,\n  variables,\n  caller,\n}: {\n  query: string;\n  variables: TVariables;\n  caller: string;\n}): Promise<TData> => {\n  const apiUrl = process.env[DEFAULT_API_URL_NAME];\n  const accessToken = process.env[DEFAULT_APP_ACCESS_TOKEN_NAME];\n\n  if (!apiUrl || !accessToken) {\n    throw new Error(\n      `${caller}() requires the app runtime env vars ` +\n        `${DEFAULT_API_URL_NAME} and ${DEFAULT_APP_ACCESS_TOKEN_NAME}.`,\n    );\n  }\n\n  const response = await fetch(`${apiUrl}/metadata`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${accessToken}`,\n    },\n    body: JSON.stringify({ query, variables }),\n  });\n\n  if (!response.ok) {\n    throw new Error(\n      `${caller}() failed: HTTP ${response.status} ${response.statusText}`,\n    );\n  }\n\n  const body = (await response.json()) as {\n    data?: TData;\n    errors?: { message: string }[];\n  };\n\n  if (body.errors && body.errors.length > 0) {\n    throw new Error(\n      `${caller}() failed: ${body.errors.map((error) => error.message).join(', ')}`,\n    );\n  }\n\n  if (!body.data) {\n    throw new Error(`${caller}() failed: response contained no data.`);\n  }\n\n  return body.data;\n};\n", "import { AppConnectionAuthFailedError } from '@/sdk/logic-function/connections/errors/app-connection-auth-failed.error';\nimport { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst GET_APP_CONNECTION_QUERY = `\n  query GetAppConnection($id: ID!) {\n    appConnection(id: $id) {\n      id\n      providerName\n      name\n      handle\n      visibility\n      userWorkspaceId\n      accessToken\n      scopes\n      authFailedAt\n    }\n  }\n`;\n\nexport const getConnection = async (id: string): Promise<AppConnection> => {\n  const { appConnection } = await postGraphqlRequest<\n    { id: string },\n    { appConnection: AppConnection }\n  >({\n    query: GET_APP_CONNECTION_QUERY,\n    variables: { id },\n    caller: 'getConnection',\n  });\n\n  if (appConnection.authFailedAt !== null) {\n    throw new AppConnectionAuthFailedError(appConnection.id);\n  }\n\n  return appConnection;\n};\n", "import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst LIST_APP_CONNECTIONS_QUERY = `\n  query ListAppConnections($filter: ListAppConnectionsInput) {\n    appConnections(filter: $filter) {\n      id\n      providerName\n      name\n      handle\n      visibility\n      userWorkspaceId\n      accessToken\n      scopes\n      authFailedAt\n    }\n  }\n`;\n\nexport type ListConnectionsFilter = {\n  providerName?: string;\n  userWorkspaceId?: string;\n  visibility?: 'user' | 'workspace';\n};\n\nexport const listConnections = async (\n  filter: ListConnectionsFilter = {},\n): Promise<AppConnection[]> => {\n  const { appConnections } = await postGraphqlRequest<\n    { filter: ListConnectionsFilter },\n    { appConnections: AppConnection[] }\n  >({\n    query: LIST_APP_CONNECTIONS_QUERY,\n    variables: { filter },\n    caller: 'listConnections',\n  });\n\n  return appConnections;\n};\n", "import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';\n\nexport const findConnectionForRequest = (\n  connections: AppConnection[],\n  event: { userWorkspaceId: string | null },\n): AppConnection | null => {\n  if (event.userWorkspaceId !== null) {\n    const personal = connections.find(\n      (connection) =>\n        connection.visibility === 'user' &&\n        connection.userWorkspaceId === event.userWorkspaceId,\n    );\n\n    if (personal) {\n      return personal;\n    }\n  }\n\n  const workspaceShared = connections.find(\n    (connection) => connection.visibility === 'workspace',\n  );\n\n  return workspaceShared ?? null;\n};\n", "import {\n  type RunAgentInput,\n  type RunAgentResult,\n} from 'twenty-shared/application';\n\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst RUN_AGENT_MUTATION = `\n  mutation RunAgent($input: RunAgentInput!) {\n    runAgent(input: $input) {\n      result\n      error\n      success\n    }\n  }\n`;\n\nexport const runAgent = async (\n  input: RunAgentInput,\n): Promise<RunAgentResult> => {\n  const { runAgent: result } = await postGraphqlRequest<\n    { input: RunAgentInput },\n    { runAgent: RunAgentResult }\n  >({\n    query: RUN_AGENT_MUTATION,\n    variables: { input },\n    caller: 'runAgent',\n  });\n\n  return result;\n};\n", "import { MetadataApiClient } from 'twenty-client-sdk/metadata';\nimport {\n  type EnqueueJobInput,\n  type EnqueueJobResult,\n} from 'twenty-shared/application';\n\nexport const enqueueJob = async (\n  input: EnqueueJobInput,\n): Promise<EnqueueJobResult> => {\n  const client = new MetadataApiClient();\n\n  const { enqueueJob: result } = await client.mutation({\n    enqueueJob: {\n      __args: { input },\n      enqueued: true,\n      logicFunctionUniversalIdentifier: true,\n    },\n  });\n\n  return result;\n};\n", "import {\n  type AppKeyValue,\n  type AppKeyValueScope,\n} from 'twenty-shared/application';\n\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst GET_APP_KEY_VALUE_QUERY = `\n  query GetAppKeyValue($key: String!, $scope: AppKeyValueScope) {\n    appKeyValue(key: $key, scope: $scope) {\n      key\n      value\n      scope\n    }\n  }\n`;\n\nconst SET_APP_KEY_VALUE_MUTATION = `\n  mutation SetAppKeyValue($input: SetAppKeyValueInput!) {\n    setAppKeyValue(input: $input) {\n      key\n      value\n      scope\n    }\n  }\n`;\n\nconst DELETE_APP_KEY_VALUE_MUTATION = `\n  mutation DeleteAppKeyValue($key: String!, $scope: AppKeyValueScope) {\n    deleteAppKeyValue(key: $key, scope: $scope)\n  }\n`;\n\nconst DEFAULT_APP_KEY_VALUE_SCOPE: AppKeyValueScope = 'WORKSPACE';\n\ntype KvOptions = {\n  scope?: AppKeyValueScope;\n};\n\nexport const kv = {\n  async get<TValue = unknown>(\n    key: string,\n    options?: KvOptions,\n  ): Promise<TValue | null> {\n    const { appKeyValue } = await postGraphqlRequest<\n      { key: string; scope: AppKeyValueScope },\n      { appKeyValue: AppKeyValue | null }\n    >({\n      query: GET_APP_KEY_VALUE_QUERY,\n      variables: { key, scope: options?.scope ?? DEFAULT_APP_KEY_VALUE_SCOPE },\n      caller: 'kv.get',\n    });\n\n    return (appKeyValue?.value ?? null) as TValue | null;\n  },\n\n  async set<TValue>(\n    key: string,\n    value: TValue,\n    options?: KvOptions,\n  ): Promise<void> {\n    await postGraphqlRequest<\n      {\n        input: { key: string; value: TValue; scope: AppKeyValueScope };\n      },\n      { setAppKeyValue: AppKeyValue }\n    >({\n      query: SET_APP_KEY_VALUE_MUTATION,\n      variables: {\n        input: {\n          key,\n          value,\n          scope: options?.scope ?? DEFAULT_APP_KEY_VALUE_SCOPE,\n        },\n      },\n      caller: 'kv.set',\n    });\n  },\n\n  async delete(key: string, options?: KvOptions): Promise<boolean> {\n    const { deleteAppKeyValue } = await postGraphqlRequest<\n      { key: string; scope: AppKeyValueScope },\n      { deleteAppKeyValue: boolean }\n    >({\n      query: DELETE_APP_KEY_VALUE_MUTATION,\n      variables: { key, scope: options?.scope ?? DEFAULT_APP_KEY_VALUE_SCOPE },\n      caller: 'kv.delete',\n    });\n\n    return deleteAppKeyValue;\n  },\n};\n", "import { type LogicFunctionHttpResponse } from 'twenty-shared/types';\n\nexport type ResponseInit = {\n  status?: number;\n  headers?: Record<string, string>;\n};\n\nexport class Response implements LogicFunctionHttpResponse {\n  readonly __twentyHttpResponse = true as const;\n  readonly body: unknown;\n  readonly status?: number;\n  readonly headers?: Record<string, string>;\n\n  constructor(body: unknown, init?: ResponseInit) {\n    this.body = body;\n    this.status = init?.status;\n    this.headers = init?.headers;\n  }\n}\n", "/**\n * Identifiers, endpoint shape and storage keys for the Numaya licensing client.\n *\n * Same permanence rule as `src/constants/universal-identifiers.ts`: every UUID\n * here is written into the customer's workspace at install time, so changing one\n * orphans the old entity rather than renaming it. Adding is safe.\n *\n * These live in their own file rather than in `universal-identifiers.ts` for the\n * same reason the gate-queue constants do \u2014 so the licensing surface can be\n * built without touching the data-model constants the scoring engine depends on.\n *\n * ---------------------------------------------------------------------------\n * ## What is verified here, and how\n *\n * Everything below is now confirmed against the **REST API of the running\n * service**, not inferred. The service publishes an OpenAPI document at\n * `https://licensing.rizvigoc.com/openapi.json` (title: *Numaya Licensing -\n * Customer API*), and each request shape below was additionally exercised over\n * real HTTP on 2026-08-04.\n *\n * The spec is authoritative over `LICENSING_INTEGRATION.md`, which was written\n * from the service's **MCP** interface and predates the published spec. Where\n * the two disagree, the spec \u2014 and the live response \u2014 wins.\n *\n * The base URL stays an **application variable** (`LICENCE_API_BASE_URL`) even\n * though it is now confirmed, because a customer running a private Numaya\n * deployment still needs to point it somewhere else, and because a wrong\n * default must remain a settings change rather than a release.\n *\n * Nothing here is load-bearing for safety: every request this client makes\n * either succeeds, or fails in a way that degrades to rules-only scoring. A\n * wrong base URL costs the customer enrichment, never a lead. See\n * `src/licensing/state.ts`.\n * ---------------------------------------------------------------------------\n */\n\n/* -------------------------------------------------------------------------- */\n/* Universal identifiers                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport const LICENCE_API_BASE_URL_APP_VARIABLE_UNIVERSAL_IDENTIFIER =\n  '0dbad279-c07c-4ae0-91e5-68cb0e87c944';\n\n/**\n * `LICENCE_ENVIRONMENT`. See `LICENCE_ENVIRONMENTS` below for why an endpoint\n * that a customer will never change is still a Layer 1 variable.\n */\nexport const LICENCE_ENVIRONMENT_APP_VARIABLE_UNIVERSAL_IDENTIFIER =\n  'e39571dd-0053-4c17-a11f-c0988985d312';\n\nexport const LICENCE_REVALIDATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  'fc2534ad-a3a1-4b05-bdf0-f2e7a1cef729';\n\nexport const LICENCE_STATUS_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  '42eaa3bc-75b6-49be-863c-a3b3c539e3a2';\n\nexport const LICENCE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =\n  '42089a05-139e-4061-aaaa-1d53e851ac90';\n\nexport const LICENCE_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  'b0316c8e-e4ad-492c-9197-f26c553c0151';\n\n/* -------------------------------------------------------------------------- */\n/* The licence panel                                                           */\n/* -------------------------------------------------------------------------- */\n\nexport const LICENCE_ROUTE_PATH = '/greenlight/licence';\n\nexport const LICENCE_CLIENT_PATH = `/s${LICENCE_ROUTE_PATH}`;\n\n/**\n * Where \"Get a licence\" sends an admin.\n *\n * A constant rather than a literal in the component because it is the one part\n * of this feature guaranteed to move: today numaya.ai has no Greenlight\n * checkout, so the honest destination is the contact route, which reaches a\n * human who can issue a key. When a real purchase page exists this is the only\n * line that changes.\n *\n * The `source` parameter is not decoration. Without it there is no way to tell\n * an enquiry that came from inside a customer's own CRM \u2014 someone who has\n * already installed the app and hit the rules-only ceiling \u2014 from a cold\n * website visitor, and those two deserve different replies.\n */\nexport const LICENCE_PURCHASE_URL =\n  'https://numaya.ai/contact?source=greenlight-app';\n\n/* -------------------------------------------------------------------------- */\n/* Endpoint shape \u2014 confirmed against the published spec and live HTTP         */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Default value of the `LICENCE_API_BASE_URL` application variable.\n *\n * `https://licensing.rizvigoc.com` is the licensing service's real public\n * hostname, confirmed on 2026-08-04 by fetching its OpenAPI document and by\n * driving `validate` and `activate` against it over HTTPS.\n *\n * The earlier default, `license.numaya.ai`, never existed: it was a placeholder\n * that entered this project's own documentation and was then cited by it. It\n * did not resolve in public DNS, so every install shipped with it fell straight\n * through the cache \u2192 grace \u2192 rules-only ladder. That ladder worked, which is\n * precisely why the wrong hostname survived as long as it did.\n */\nexport const DEFAULT_LICENCE_API_BASE_URL = 'https://licensing.rizvigoc.com';\n\n/**\n * Confirmed. Both paths appear verbatim in the service's published OpenAPI\n * document and both were exercised live. The `/v1` prefix and the US spelling\n * `licenses` are the service's, not ours.\n */\nexport const LICENCE_VALIDATE_PATH = '/v1/licenses/validate';\nexport const LICENCE_ACTIVATE_PATH = '/v1/licenses/activate';\n\n/* -------------------------------------------------------------------------- */\n/* Environment routing                                                         */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The header that selects which side of the licensing service answers.\n *\n * One host serves both environments. Sandbox licences are **only** visible to a\n * request carrying `X-Numaya-Environment: sandbox`; without it the same key\n * comes back `200 valid:false reason:\"license_not_found\"`. The service's own\n * spec says so in one sentence \u2014 \"Use `X-Numaya-Environment: sandbox` header or\n * `nml_test_` API keys to access the sandbox\" \u2014 and the live behaviour matches.\n *\n * The header value is matched case-insensitively by the service (`sandbox`,\n * `Sandbox` and `SANDBOX` all worked); we send the lower-case form.\n */\nexport const LICENCE_ENVIRONMENT_HEADER = 'X-Numaya-Environment';\n\n/**\n * The two environments, and why this is an application variable at all.\n *\n * It is **not** derivable from the base URL \u2014 the same host serves both \u2014 so it\n * cannot be folded into `LICENCE_API_BASE_URL`. It cannot be a code constant\n * either: our own CI workspace and every throwaway dev container is licensed\n * out of sandbox, and making that a constant would mean a code change and a\n * republish to test a licensing change. And it cannot live in Layer 2, the\n * CRM-editable `GreenlightConfig` record, because the post-install hook\n * validates the licence before any Layer 2 record is guaranteed to exist.\n *\n * That leaves Layer 1. A customer never touches it \u2014 the default is\n * `production` and the description says as much \u2014 but it has to be *settable*\n * without a rebuild, which is the identical argument that already put\n * `LICENCE_API_BASE_URL` in Layer 1.\n */\nexport const LICENCE_ENVIRONMENTS = ['production', 'sandbox'] as const;\n\nexport const DEFAULT_LICENCE_ENVIRONMENT = 'production';\n\n/**\n * The service's `reason` when a key authenticated but resolved to no licence in\n * the environment the request was routed to. US spelling, the service's own.\n *\n * This is the misrouted-environment signal. See `src/licensing/entitlement.ts`\n * for why it cannot mean \"the key has a typo\".\n */\nexport const LICENCE_NOT_FOUND_REASON = 'license_not_found';\n\n/**\n * Request body field names.\n *\n * These are *not* guesses: they are the parameter names the licensing service's\n * own MCP tool schema declares for `numaya_validate_license` and\n * `numaya_activate_license`, which are generated from the service's parameter\n * model. The REST layer using different names is possible but unlikely.\n */\nexport const LICENCE_REQUEST_FIELDS = {\n  key: 'key',\n  deviceFingerprint: 'deviceFingerprint',\n  feature: 'feature',\n  deviceName: 'deviceName',\n} as const;\n\n/**\n * The feature name the paid capability is licensed under. `rules` \u2014 the\n * deterministic gate \u2014 is on every policy and is never checked, because rules\n * scoring is the free floor and must run with no licence at all.\n */\nexport const ENRICHMENT_FEATURE_NAME = 'enrichment';\n\n/* -------------------------------------------------------------------------- */\n/* App key-value storage                                                       */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Where the cached entitlement and the published admin-facing state live.\n *\n * `scope: 'WORKSPACE'` throughout \u2014 the licence is per workspace, and the\n * activation slot is claimed with the workspace id as its fingerprint. A read\n * under a different scope silently finds nothing, which is the failure mode\n * `release-marker-store.ts` documents.\n */\nexport const LICENCE_CACHE_KV_KEY = 'greenlight.licence.cache';\n\n/**\n * Sticky record of what happened the last time we tried to claim an activation\n * slot. Separate from the validation cache on purpose: a `409` is durable\n * customer state (the licence is bound to a different workspace) and must\n * survive every nightly re-validation, whereas the validation cache is replaced\n * nightly. See `src/licensing/state.ts` for why this matters.\n */\nexport const LICENCE_ACTIVATION_KV_KEY = 'greenlight.licence.activation';\n\n/**\n * The resolved licence state, published for the admin UI to read. Written on\n * every validation; never contains the licence key.\n */\nexport const LICENCE_STATE_KV_KEY = 'greenlight.licence.state';\n\n/**\n * The verified calibration payload, cached beside the entitlement.\n *\n * Separate from `LICENCE_CACHE_KV_KEY` on purpose. The two are written by the\n * same nightly run but answer different questions and age on different clocks \u2014\n * the entitlement tightens at 24h because a stale one risks serving a paid\n * feature to a revoked licence, while calibration survives to 72h because it is\n * a word list and dropping it early would move a customer's scores for a reason\n * that has nothing to do with their data. See `src/calibration/state.ts`.\n *\n * It holds no key, no entitlement and nothing secret: the payload is the same\n * vocabulary every licensed workspace receives.\n */\nexport const CALIBRATION_CACHE_KV_KEY = 'greenlight.calibration.cache';\n\n/**\n * The resolved calibration state, published for the admin panel: shipped\n * defaults or calibration vNN, from where, and when it last refreshed.\n */\nexport const CALIBRATION_STATE_KV_KEY = 'greenlight.calibration.state';\n\n/* -------------------------------------------------------------------------- */\n/* Schedule                                                                    */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Nightly re-validation, 03:17 UTC.\n *\n * Standard five-field CRON. The odd minute is deliberate: every Greenlight\n * install would otherwise phone home on the same second, and the licensing\n * service is a single small container. Daily is the right frequency because the\n * cache TTL is 24h \u2014 validating more often buys nothing, validating less often\n * would let the cache go stale before the next run.\n */\nexport const LICENCE_REVALIDATE_CRON_PATTERN = '17 3 * * *';\n", "/**\n * How licence state reaches a human \u2014 the audit row and the admin notice.\n *\n * ===========================================================================\n * ## Why this is a problem worth solving rather than a log line\n *\n * The whole point of failing open is that a licence problem is *quiet*: the\n * product keeps working, leads keep scoring, nobody's day breaks. That is\n * exactly what makes it dangerous. An expired licence that silently downgrades\n * the product produces a support ticket six weeks later reading \"enrichment\n * stopped working at some point, we don't know when\" \u2014 and by then the audit\n * trail is the only way to answer it.\n *\n * So every validation is recorded, including the boring ones. One row a night\n * is 365 rows a year against an object built for one row per scored lead; the\n * cost is nil and the alternative \u2014 only recording changes \u2014 means the row you\n * want is the one that was suppressed because \"nothing changed\".\n *\n * ## Where it surfaces\n *\n * Three places, in descending order of how likely an admin is to see them:\n *\n *  1. **`GreenlightAuditLog`** \u2014 the object already has a view and a sidebar\n *     item, is sorted newest-first, and is the place admins are already told to\n *     look when they ask \"why did this happen\". Licence rows land there next to\n *     scoring decisions, which is right: from the customer's point of view they\n *     are the same story.\n *  2. **App key-value storage** (`LICENCE_STATE_KV_KEY`) \u2014 the resolved state,\n *     republished on every run, for the Greenlight main page to render as a\n *     banner. Cheap to read, always current, no query.\n *  3. **Structured logs** \u2014 `logGreenlight('licence_resolved', \u2026)`, for support\n *     with server access.\n *\n * ## The event type is `LICENCE`, for every severity\n *\n * These rows used to file under `CONFIG_CHANGED` when healthy and `ERROR` when\n * not, because `GreenlightAuditLog.eventType` had no option of its own. It has\n * one now, and both halves of that compromise are gone rather than one:\n *\n *   - `CONFIG_CHANGED` was never true. Nobody changed any configuration; a\n *     scheduled check ran and reported what the licence server already thought.\n *   - `ERROR` is worse. `eventType` says *what happened*, and what happened is a\n *     licence check \u2014 in exactly the way `scoring-run.ts` files a held lead as\n *     `GATED` and leaves the `decision` column to say which kind of hold it was.\n *     Reserving `ERROR` for \"Greenlight itself failed\" is what keeps a red row\n *     meaning something: an expired licence is a customer's commercial state,\n *     not a fault in this app, and filing it as a fault is how an admin learns\n *     to ignore red.\n *\n * The severity survives the move and is not inferred from the event type: it is\n * carried explicitly in `ruleTrace.severity`, and the row's summary leads with\n * the admin-facing headline. `LICENCE` is yellow in the SELECT, which is the\n * colour for \"look at this\" without the claim that something broke.\n *\n * Every row's summary still starts with a `LICENCE` prefix, so the text search\n * that isolated these rows before the option existed keeps working.\n *\n * ## Nothing here may contain the licence key\n *\n * `LicenceState` has no field that could hold one \u2014 it carries `maskedKey`, and\n * `maskLicenceKey` emits no characters of the secret. This module never sees the\n * raw key, so there is no code path from the key to a row. That is asserted in\n * `__tests__/audit.test.ts` by scanning the serialised row for the sandbox keys.\n * ===========================================================================\n */\n\nimport {\n  type LicenceEnvironmentName,\n  type LicenceSeverity,\n  type LicenceState,\n} from 'src/licensing/types';\n\nexport const LICENSING_CLIENT_VERSION = '0.1.0';\n\n/** Prefix on every licence audit summary, so the rows are greppable today. */\nexport const LICENCE_AUDIT_PREFIX = 'LICENCE';\n\n/* -------------------------------------------------------------------------- */\n/* Admin-facing description                                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface LicenceNotice {\n  readonly severity: LicenceSeverity;\n  /** One line, suitable as a banner title or an audit row summary. */\n  readonly headline: string;\n  /** One or two sentences telling the admin what to actually do. */\n  readonly detail: string;\n}\n\nconst daysPhrase = (daysRemaining: number | null): string =>\n  daysRemaining === null\n    ? 'soon'\n    : daysRemaining <= 0\n      ? 'today'\n      : daysRemaining === 1\n        ? 'in 1 day'\n        : `in ${daysRemaining} days`;\n\nconst hoursSince = (cacheAgeMs: number | null): string =>\n  cacheAgeMs === null ? 'never' : `${Math.floor(cacheAgeMs / 3_600_000)}h ago`;\n\n/**\n * States published before `environment` existed read back as `undefined`. The\n * fallback keeps a stale row readable rather than rendering \"the undefined\n * environment\" at the admin; the next nightly run replaces it with the truth.\n */\nconst environmentLabel = (environment: LicenceEnvironmentName): string =>\n  environment === 'sandbox' ? 'sandbox' : 'production';\n\n/** The setting to try, named rather than implied. */\nconst otherEnvironmentLabel = (environment: LicenceEnvironmentName): string =>\n  environment === 'sandbox' ? 'production' : 'sandbox';\n\n/**\n * Turn a resolved state into something an admin can act on.\n *\n * Every `detail` names the next action. A notice that says \"your licence is\n * invalid\" and stops is a notice that generates a support ticket; one that says\n * which lever to pull does not.\n */\nexport const describeLicenceForAdmin = (state: LicenceState): LicenceNotice => {\n  const expiring =\n    state.expiryWarning ||\n    (state.daysRemaining !== null && state.daysRemaining <= 7);\n\n  switch (state.reason) {\n    case 'licensed':\n      return expiring\n        ? {\n            severity: state.severity,\n            headline: `Greenlight licence expires ${daysPhrase(state.daysRemaining)}`,\n            detail:\n              'Enrichment is running normally. Renew before the expiry date to avoid dropping to rules-only scoring. Deterministic scoring and the gate continue either way.',\n          }\n        : {\n            severity: state.severity,\n            headline: 'Greenlight licence active',\n            detail: `Enrichment is enabled${state.tier === null ? '' : ` on the ${state.tier} tier`}.`,\n          };\n\n    case 'no_licence_key':\n      return {\n        severity: state.severity,\n        headline: 'Greenlight is running in rules-only mode',\n        detail:\n          'No licence key is configured, so AI and search enrichment are off. Deterministic scoring, the gate and the queue all work without one. Add a key in the app settings to enable enrichment.',\n      };\n\n    case 'feature_not_licensed':\n      return {\n        severity: state.severity,\n        headline: 'Enrichment is not included in this licence',\n        detail:\n          'The licence is valid and in good standing, but does not include the enrichment capability. Rules-only scoring is unaffected. Upgrade the licence to turn enrichment on.',\n      };\n\n    // The one notice in this file that names a Greenlight setting as the likely\n    // culprit, because it is the one failure the service reports with a message\n    // that reads like a customer problem when it is almost always ours. It says\n    // which environment was asked, what to change, and \u2014 explicitly \u2014 that the\n    // key is not the thing to go and re-check. See gotcha 4 in `entitlement.ts`.\n    case 'licence_not_found_in_environment':\n      return {\n        severity: state.severity,\n        headline: `Licence key was accepted but no licence exists in the ${environmentLabel(state.environment)} environment`,\n        detail:\n          `This is almost certainly a misconfiguration, not a bad key: the licensing service accepted the key as valid credentials, then found no licence attached to it in the ${environmentLabel(state.environment)} environment. Set LICENCE_ENVIRONMENT to ${otherEnvironmentLabel(state.environment)} in the app settings if this workspace is licensed there, and re-run the licence check. Do not re-enter the key \u2014 a wrong or mistyped key is rejected outright and reports \"licence was rejected\" instead. If the environment is already correct, the licence has been deleted and Numaya AI support can restore it. Scoring, the gate and the queue are unaffected throughout.`,\n      };\n\n    case 'expired':\n      return {\n        severity: state.severity,\n        headline: 'Greenlight licence has expired',\n        detail:\n          'Enrichment is off. Scoring, the gate and the queue continue to work. Renew the licence to restore enrichment.',\n      };\n\n    case 'suspended':\n      return {\n        severity: state.severity,\n        headline: 'Greenlight licence is suspended',\n        detail:\n          'Enrichment is off. This is usually a billing issue and is reversible \u2014 contact Numaya AI support. Scoring is unaffected.',\n      };\n\n    case 'revoked':\n      return {\n        severity: state.severity,\n        headline: 'Greenlight licence has been revoked',\n        detail:\n          'Enrichment is off and this cannot be reversed on the existing key. Contact Numaya AI support for a replacement. Scoring is unaffected.',\n      };\n\n    case 'quota_exceeded':\n      return {\n        severity: state.severity,\n        headline: 'Greenlight enrichment quota reached',\n        detail:\n          'Enrichment is paused until the quota period resets. Scoring is unaffected.',\n      };\n\n    case 'device_not_activated':\n      return {\n        severity: state.severity,\n        headline: 'This workspace is not activated against the licence',\n        detail:\n          'Reinstall the app, or contact Numaya AI support, to claim an activation slot. Scoring is unaffected.',\n      };\n\n    case 'max_activations_reached':\n      return {\n        severity: state.severity,\n        headline: 'Licence is already in use by another workspace',\n        detail:\n          'Every activation slot on this licence is taken, so this workspace runs rules-only. Free a slot on the other workspace or add a seat, then reinstall Greenlight here.',\n      };\n\n    case 'invalid_licence':\n      return {\n        severity: state.severity,\n        headline: 'Greenlight licence was rejected',\n        detail:\n          'The licensing service refused this key outright \u2014 it does not recognise it in any environment. Check the key in the app settings for a typo or a truncated paste, then contact Numaya AI support. Scoring is unaffected.',\n      };\n\n    case 'licence_service_rate_limited':\n      return {\n        severity: state.severity,\n        headline: 'Licence check was rate-limited',\n        detail:\n          'Numaya returned HTTP 429, so this workspace could not re-check its licence. Enrichment is paused until the next nightly check succeeds; scoring, the gate and the queue are unaffected. Nothing needs doing unless this persists for several days.',\n      };\n\n    case 'licence_response_malformed':\n      return {\n        severity: state.severity,\n        headline: 'Licence service returned an unreadable response',\n        detail:\n          'The licensing service answered, but not in a shape this version understands \u2014 usually a proxy or an upgrade in progress. Enrichment is paused; scoring is unaffected. Report it to Numaya AI support if it persists.',\n      };\n\n    case 'licence_service_unreachable':\n      return {\n        severity: state.severity,\n        headline: 'Cannot reach the Numaya AI licensing service',\n        detail: `Enrichment is paused while the cached entitlement ages out (last successful check ${hoursSince(state.cacheAgeMs)}). Check outbound network access to the licensing service. Scoring is unaffected.`,\n      };\n\n    case 'licence_service_unreachable_grace_expired':\n      return {\n        severity: state.severity,\n        headline:\n          state.cacheAgeMs === null\n            ? 'Greenlight has never successfully checked its licence'\n            : 'Licence has not been verified for over 72 hours',\n        detail: `Enrichment is off until a licence check succeeds (last successful check ${hoursSince(state.cacheAgeMs)}). Confirm LICENCE_API_BASE_URL and outbound network access. Scoring, the gate and the queue are unaffected and will keep running indefinitely.`,\n      };\n  }\n};\n\n/* -------------------------------------------------------------------------- */\n/* Audit row                                                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The structured payload written to `ruleTrace`. This is the row that answers\n * \"what did Numaya say, and what did we do about it, at 03:17 on the 14th\".\n *\n * Carries `valid` and `hasFeature` as separate fields on purpose \u2014 collapsing\n * them into the resulting mode would erase precisely the distinction that the\n * independence gotcha makes load-bearing.\n */\nexport interface LicenceAuditDetail {\n  readonly checkedAt: string;\n  readonly maskedKey: string;\n  /**\n   * Recorded on every row, healthy ones included. A row that says\n   * `reason: license_not_found` without saying which environment was asked is a\n   * support ticket; with it, it is a two-minute fix.\n   */\n  readonly environment: string;\n  readonly valid: boolean | null;\n  readonly hasFeature: boolean | null;\n  readonly reason: string;\n  readonly source: 'live' | 'cache' | 'grace' | 'none';\n  readonly cached: boolean;\n  readonly cacheAgeHours: number | null;\n  readonly mode: string;\n  readonly severity: LicenceSeverity;\n  readonly failureKind: string | null;\n  readonly tier: string | null;\n  readonly daysRemaining: number | null;\n  readonly expiryWarning: boolean;\n  readonly expiryAt: string | null;\n}\n\nexport const buildLicenceAuditDetail = (\n  state: LicenceState,\n): LicenceAuditDetail => ({\n  checkedAt: state.checkedAt,\n  maskedKey: state.maskedKey,\n  environment: environmentLabel(state.environment),\n  valid: state.observedValid,\n  hasFeature: state.observedHasFeature,\n  reason: state.reason,\n  source: state.source,\n  // Spelled out as a boolean as well as a source, because \"cached vs live\" is\n  // the question support asks and `source: 'grace'` does not obviously answer\n  // it.\n  cached: state.source !== 'live',\n  cacheAgeHours:\n    state.cacheAgeMs === null\n      ? null\n      : Math.round((state.cacheAgeMs / 3_600_000) * 10) / 10,\n  mode: state.mode,\n  severity: state.severity,\n  failureKind: state.failureKind,\n  tier: state.tier,\n  daysRemaining: state.daysRemaining,\n  expiryWarning: state.expiryWarning,\n  expiryAt: state.expiryAt,\n});\n\n/**\n * The `data` argument for `createGreenlightAuditLog`.\n *\n * Shaped to match the rows `install-run.ts` writes: the lead-pointer fields are\n * empty strings rather than omitted, because the object's `SELECT` fields have\n * literal defaults and a partially-populated row reads worse in the view than a\n * deliberately blank one. `band` and `decision` are `UNSCORED` for the same\n * reason \u2014 this row describes no lead, and `UNSCORED` is the vocabulary's word\n * for \"not applicable\".\n */\nexport const buildLicenceAuditRow = (\n  state: LicenceState,\n  /**\n   * The calibration outcome of the same run, if there was one.\n   *\n   * Passed in as a loose record rather than typed against\n   * `src/calibration/types.ts` for the same reason `LicenceValidationResponse`\n   * types `customerMetadata` as `unknown`: the licensing core must not acquire\n   * a dependency on what calibration is. It reads three fields for display and\n   * copies the rest verbatim into the trace.\n   *\n   * It is on the row rather than only in the panel because an audit row outlives\n   * the panel's \"now\". \"Which vocabulary was this workspace scoring on last\n   * March\" is a question asked months later, and the only place that can answer\n   * it is the append-only trail.\n   */\n  calibration?: {\n    readonly status?: unknown;\n    readonly reason?: unknown;\n    readonly calibrationVersion?: unknown;\n    readonly refreshedAt?: unknown;\n  } | null,\n): Record<string, unknown> => {\n  const notice = describeLicenceForAdmin(state);\n\n  // \"calibration v1\" or \"shipped defaults\" \u2014 the whole answer to \"am I running\n  // the calibrated lists?\", short enough to sit in a row name in a list view.\n  const calibrationLabel =\n    calibration === null || calibration === undefined\n      ? null\n      : calibration.status === 'calibrated' &&\n          typeof calibration.calibrationVersion === 'number'\n        ? `calibration v${calibration.calibrationVersion}`\n        : 'shipped defaults';\n\n  return {\n    name:\n      calibrationLabel === null\n        ? `${LICENCE_AUDIT_PREFIX} \u00B7 ${notice.headline} \u00B7 ${state.mode}`\n        : `${LICENCE_AUDIT_PREFIX} \u00B7 ${notice.headline} \u00B7 ${state.mode} \u00B7 ${calibrationLabel}`,\n    // One value for every severity. See the module comment: the severity lives\n    // in `ruleTrace.severity` and in the headline above, not in a borrowed\n    // event type.\n    eventType: 'LICENCE',\n    occurredAt: state.checkedAt,\n    leadObjectNameSingular: '',\n    leadDisplayName: '',\n    actorType: 'SYSTEM',\n    actorDisplayName: `Greenlight licensing ${LICENSING_CLIENT_VERSION}`,\n    ruleTrace: {\n      ...buildLicenceAuditDetail(state),\n      adminDetail: notice.detail,\n      ...(calibration === null || calibration === undefined\n        ? {}\n        : { calibration }),\n    },\n    score: null,\n    band: 'UNSCORED',\n    decision: 'UNSCORED',\n    overrideReason: '',\n    traceId: '',\n  };\n};\n", "/**\n * The feature gate \u2014 the only sanctioned way to ask \"may enrichment run\".\n *\n * ===========================================================================\n * ## Deterministic scoring is not gated. There is no switch for it.\n *\n * Look for a `isScoringEnabled` here and you will not find one, and that is the\n * design rather than an omission. Greenlight's deterministic gate is the free\n * floor: it runs with no licence key, with an expired licence, with a revoked\n * licence, and during a total Numaya outage. A licence problem degrades the\n * product; it must never stop leads being scored, because the lead is the\n * customer's asset and we do not get to hold it hostage over our billing.\n *\n * The enforcement is structural, not documentary:\n *\n *  - `scoring-run.ts` does not import this module, and nothing in\n *    `src/scoring/` does either. There is no code path from a licence state to\n *    a scoring decision.\n *  - This module's only consumers are enrichment-side.\n *  - `__tests__/scoring-unaffected.test.ts` scores the same lead under every\n *    licence state this client can produce and asserts an identical result.\n *\n * ## Why this exists before enrichment does\n *\n * Enrichment lands in v0.2. Wiring speculative calls into a subsystem that does\n * not exist would mean inventing its shape now and being wrong; shipping the\n * licensing client without any consumer would mean the gate's semantics get\n * decided later, in a hurry, by whoever writes the first enrichment call \u2014 and\n * the two `valid`/`hasFeature` gotchas are exactly the sort of thing that gets\n * decided wrongly in a hurry.\n *\n * So the predicate is named, tested and documented now, and v0.2 consumes it:\n *\n * ```ts\n * const licence = await readPublishedLicenceState(store);\n *\n * if (!isEnrichmentEnabled(licence)) {\n *   return { score, scoreOnly: true, reason: enrichmentDisabledReason(licence) };\n * }\n * ```\n *\n * Note that even that call is not on the scoring path \u2014 it sits after the score\n * is computed, deciding only whether to *add* to it.\n * ===========================================================================\n */\n\nimport {\n  type LicenceMode,\n  type LicenceReason,\n  type LicenceState,\n} from 'src/licensing/types';\n\n/**\n * The predicate v0.2's enrichment path consumes.\n *\n * Takes the resolved state rather than a raw response on purpose: by this point\n * the `valid && hasFeature` conjunction, the cache ladder and the sticky\n * activation override have all been applied. A caller cannot accidentally\n * reconstruct the wrong gate, because the only thing it is handed is the answer.\n *\n * `null` \u2014 no state has ever been published, i.e. the app has never completed a\n * licence check \u2014 reads as disabled. That is the safe direction for a paid\n * feature and the honest one: we do not know that this workspace is entitled.\n */\nexport const isEnrichmentEnabled = (state: LicenceState | null): boolean =>\n  state !== null && state.mode === 'full';\n\n/**\n * Why enrichment is off, for the trace and the UI. `null` when it is on.\n */\nexport const enrichmentDisabledReason = (\n  state: LicenceState | null,\n): LicenceReason | null => {\n  if (state === null) {\n    return 'licence_service_unreachable_grace_expired';\n  }\n\n  return state.mode === 'full' ? null : state.reason;\n};\n\n/**\n * Everything an enrichment caller needs in one read, so it never has to touch\n * `state.mode` or `state.reason` directly.\n */\nexport interface EnrichmentGate {\n  readonly enabled: boolean;\n  readonly mode: LicenceMode;\n  readonly reason: LicenceReason | null;\n}\n\nexport const describeEnrichmentGate = (\n  state: LicenceState | null,\n): EnrichmentGate => ({\n  enabled: isEnrichmentEnabled(state),\n  mode: state === null ? 'rules-only' : state.mode,\n  reason: enrichmentDisabledReason(state),\n});\n\n/**\n * Stated as executable documentation, and asserted in the test suite.\n *\n * It exists so that a future change which *does* try to gate scoring has to\n * delete a function whose name says what it is doing, rather than quietly adding\n * an `if`.\n */\nexport const isDeterministicScoringLicensed = (): true => true;\n", "/**\n * Licence-key masking.\n *\n * `LICENCE_KEY` is a secret application variable. Twenty keeps it out of its own\n * UI and logs, but nothing stops *us* writing it into a structured log line, an\n * audit row's `ruleTrace`, or an error message \u2014 and an audit row is queryable\n * by every workspace admin and survives forever. So the key is masked exactly\n * once, at the edge of the run, and only the mask travels inwards.\n *\n * ## Why the mask keeps no key characters at all\n *\n * The obvious mask \u2014 \"first four, last four\" \u2014 is the wrong trade here. A key\n * is shaped `NUMAYA-XXXXX-XXXXX-XXXXX-X`: the last group is a single character and\n * the whole secret is about nineteen base-32 characters, so showing eight of\n * them is not a redaction, it is a discount. What support actually needs from a\n * mask is only ever *\"is this the same key as the one in that other log line\"*,\n * and a fingerprint answers that without revealing anything.\n *\n * So the mask is:\n *\n *   `NUMAYA-*****-*****-*****-* #6f1c2a9b`\n *\n * \u2014 group *structure* preserved (which makes a malformed key obvious at a\n * glance, e.g. someone pasting an invoice number), zero secret characters, and a\n * stable 32-bit fingerprint that distinguishes two keys from one another.\n *\n * ## Why FNV-1a and not a real hash\n *\n * This module is part of the pure core and may not import `node:crypto` \u2014 the\n * same rule that keeps `src/scoring/` runnable anywhere. FNV-1a is eleven lines\n * and deterministic. It is emphatically *not* a security primitive, and it does\n * not need to be: it never guards anything, it only labels. The key's entropy is\n * not protected by the hash being strong, it is protected by the hash being\n * lossy \u2014 32 bits out of ~95 bits of key means the fingerprint identifies a\n * gigantic equivalence class, not a key.\n */\n\nconst FNV_OFFSET_BASIS = 0x811c9dc5;\nconst FNV_PRIME = 0x01000193;\n\nconst fingerprint = (value: string): string => {\n  let hash = FNV_OFFSET_BASIS;\n\n  for (let index = 0; index < value.length; index += 1) {\n    hash ^= value.charCodeAt(index);\n    // Multiply in 32-bit space without overflowing into float precision.\n    hash = Math.imul(hash, FNV_PRIME) >>> 0;\n  }\n\n  return hash.toString(16).padStart(8, '0');\n};\n\n/**\n * The one product prefix that carries no information \u2014 every Greenlight key\n * starts with it, so echoing it back reveals nothing and makes the mask\n * readable. Any other leading group is masked like the rest.\n */\nconst NON_SECRET_PREFIX = 'NUMAYA';\n\nexport const MISSING_LICENCE_MASK = '(no licence key)';\n\n/**\n * Mask a licence key for logging, audit rows and error messages.\n *\n * Total: an empty, whitespace-only, `undefined` or non-string input returns\n * `MISSING_LICENCE_MASK` rather than throwing, because the commonest reason to\n * call this is precisely that the admin has not set the variable yet.\n */\nexport const maskLicenceKey = (key: string | null | undefined): string => {\n  if (typeof key !== 'string') {\n    return MISSING_LICENCE_MASK;\n  }\n\n  const trimmed = key.trim();\n\n  if (trimmed.length === 0) {\n    return MISSING_LICENCE_MASK;\n  }\n\n  const shape = trimmed\n    .split('-')\n    .map((group, index) =>\n      index === 0 && group.toUpperCase() === NON_SECRET_PREFIX\n        ? NON_SECRET_PREFIX\n        : '*'.repeat(group.length),\n    )\n    .join('-');\n\n  return `${shape} #${fingerprint(trimmed)}`;\n};\n\n/**\n * Last line of defence: strip any occurrence of the key from text that is about\n * to be logged or stored.\n *\n * `maskLicenceKey` is the primary mechanism and this is the belt to its braces \u2014\n * it exists because error messages are written by code we do not own. A `fetch`\n * rejection, a JSON parse error or a future SDK could all echo a request body,\n * and the URL-encoded form of a key is a plausible variant, so both are\n * replaced.\n *\n * Deliberately a no-op when there is no key: passing an empty string must not\n * turn every character of the message into a mask.\n */\nexport const redactLicenceKey = (\n  text: string,\n  key: string | null | undefined,\n): string => {\n  if (typeof key !== 'string') {\n    return text;\n  }\n\n  const trimmed = key.trim();\n\n  if (trimmed.length < 4) {\n    return text;\n  }\n\n  const mask = maskLicenceKey(trimmed);\n\n  return text\n    .split(trimmed)\n    .join(mask)\n    .split(encodeURIComponent(trimmed))\n    .join(mask);\n};\n", "/**\n * What the licence panel shows.\n *\n * The panel answers three questions an admin actually has \u2014 what am I running,\n * what am I not getting, and how do I fix it \u2014 and this module answers them as\n * a pure function of the published state so the answers can be tested without\n * a workspace, a network or a clock.\n *\n * ## Why \"no key\" and \"key present but not entitled\" must not read alike\n *\n * Both land in rules-only mode, and collapsing them is the obvious economy. It\n * is also the one that costs a support ticket every time: an admin who never\n * bought a licence needs a purchase route, and an admin whose key is being\n * refused needs to know *which* refusal it is before they buy a second one.\n * `describeLicenceForAdmin` already separates them; this module's job is to\n * carry that separation as far as the button.\n *\n * ## Why the call to action is data rather than markup\n *\n * `callToAction` is `null` whenever a licence is working. A panel that shows a\n * \"Get a licence\" button to a customer who already pays is not a small blemish\n * \u2014 it is the app failing to know something it definitely knows, in front of\n * the person who paid.\n */\n\nimport { describeLicenceForAdmin } from 'src/licensing/audit';\nimport { describeEnrichmentGate } from 'src/licensing/feature-gate';\nimport { MISSING_LICENCE_MASK } from 'src/licensing/mask';\nimport type {\n  LicenceMode,\n  LicenceReason,\n  LicenceSeverity,\n  LicenceState,\n} from 'src/licensing/types';\n\nexport interface LicencePanelCallToAction {\n  /** Button text. Named for what happens, not for the state that produced it. */\n  readonly label: string;\n  readonly url: string;\n}\n\nexport interface LicencePanelView {\n  readonly mode: LicenceMode;\n  readonly reason: LicenceReason | null;\n  readonly headline: string;\n  readonly detail: string;\n  readonly severity: LicenceSeverity;\n  /** Enrichment is the only thing a licence buys. Named, not implied. */\n  readonly enrichmentEnabled: boolean;\n  readonly maskedKey: string;\n  readonly environment: string | null;\n  readonly checkedAt: string | null;\n  readonly callToAction: LicencePanelCallToAction | null;\n}\n\n/**\n * The state a workspace is in before the nightly run has ever completed.\n *\n * `readPublishedLicenceState()` returns `null` both when no check has run yet\n * and when the stored value is unreadable. Treating that as \"rules-only, and we\n * have not checked\" is honest in both cases, and it is the same direction\n * `isEnrichmentEnabled` already fails in.\n */\nconst NEVER_CHECKED_HEADLINE = 'Greenlight has not checked a licence yet';\n\nconst NEVER_CHECKED_DETAIL =\n  'No licence check has completed on this workspace, so enrichment is off. Deterministic scoring, the gate and the queue all work without one. If you have just installed the app, the nightly check will run within 24 hours; adding a licence key in the app settings checks immediately.';\n\nexport const buildLicencePanelView = ({\n  state,\n  hasKeyConfigured,\n  purchaseUrl,\n}: {\n  readonly state: LicenceState | null;\n  /** Whether `LICENCE_KEY` is set, read from the app variable, never its value. */\n  readonly hasKeyConfigured: boolean;\n  readonly purchaseUrl: string;\n}): LicencePanelView => {\n  const gate = describeEnrichmentGate(state);\n\n  if (state === null) {\n    return {\n      mode: gate.mode,\n      reason: gate.reason,\n      headline: NEVER_CHECKED_HEADLINE,\n      detail: NEVER_CHECKED_DETAIL,\n      severity: 'notice',\n      enrichmentEnabled: gate.enabled,\n      maskedKey: hasKeyConfigured ? '' : MISSING_LICENCE_MASK,\n      environment: null,\n      checkedAt: null,\n      // A key is configured but unverified: sending that admin to buy another\n      // one would be wrong. Only the workspace with no key at all gets the\n      // purchase route here.\n      callToAction: hasKeyConfigured\n        ? null\n        : { label: 'Get a licence', url: purchaseUrl },\n    };\n  }\n\n  const notice = describeLicenceForAdmin(state);\n\n  return {\n    mode: state.mode,\n    reason: gate.reason,\n    headline: notice.headline,\n    detail: notice.detail,\n    severity: state.severity,\n    enrichmentEnabled: gate.enabled,\n    maskedKey: state.maskedKey,\n    environment: state.environment ?? null,\n    checkedAt: state.checkedAt,\n    callToAction: buildCallToAction(state.reason, purchaseUrl),\n  };\n};\n\n/**\n * Which states earn a purchase button.\n *\n * Only the two that a purchase would actually fix: no key at all, and a key\n * whose plan does not include enrichment. Everything else \u2014 an expired licence,\n * a rejected key, an unreachable service, a wrong environment \u2014 is a state\n * where buying again either changes nothing or makes the mess worse, and where\n * `describeLicenceForAdmin`'s detail already names the real next step.\n *\n * `licensed` returns null on purpose, including while expiring: renewal is a\n * conversation with whoever holds the contract, and a button labelled \"Get a\n * licence\" is the wrong thing to show someone who has one.\n */\nconst buildCallToAction = (\n  reason: LicenceReason,\n  purchaseUrl: string,\n): LicencePanelCallToAction | null => {\n  switch (reason) {\n    case 'no_licence_key':\n      return { label: 'Get a licence', url: purchaseUrl };\n    case 'feature_not_licensed':\n      return { label: 'Add enrichment to your plan', url: purchaseUrl };\n    default:\n      return null;\n  }\n};\n", "/**\n * The narrowest possible view of Twenty's Core API client.\n *\n * `CoreApiClient` from `twenty-client-sdk/core` is typed `any` until\n * `dev:generate-client` regenerates it against the installed workspace schema,\n * so depending on it directly buys no type safety and costs testability \u2014 the\n * class reads `API_URL` / `APP_ACCESS_TOKEN` from the process environment in its\n * constructor, which no unit test has.\n *\n * Everything in this folder therefore talks to `GreenlightApiClient`. The\n * `define*` files are the only place `new CoreApiClient()` appears, and they do\n * nothing but hand it to a handler that can be driven by a fake in tests.\n */\n\nexport interface GreenlightApiClient {\n  query(request: Record<string, unknown>): Promise<unknown>;\n  mutation(request: Record<string, unknown>): Promise<unknown>;\n}\n\nexport const isPlainRecord = (\n  value: unknown,\n): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Twenty returns connections as `{ edges: [{ node }] }`. Pull the nodes out\n * without assuming the shape is correct \u2014 a schema drift should degrade to\n * \"no records found\", never throw.\n */\nexport const readConnectionNodes = (\n  payload: unknown,\n  connectionName: string,\n): Record<string, unknown>[] => {\n  if (!isPlainRecord(payload)) {\n    return [];\n  }\n\n  const connection = payload[connectionName];\n\n  if (!isPlainRecord(connection)) {\n    return [];\n  }\n\n  const edges = connection['edges'];\n\n  if (!Array.isArray(edges)) {\n    return [];\n  }\n\n  return edges.flatMap((edge): Record<string, unknown>[] => {\n    if (!isPlainRecord(edge)) {\n      return [];\n    }\n\n    const node = edge['node'];\n\n    return isPlainRecord(node) ? [node] : [];\n  });\n};\n\n/** ISO-8601 sorts lexicographically, so this needs no Date parsing. */\nexport const oldestByCreatedAt = (\n  records: readonly Record<string, unknown>[],\n): Record<string, unknown> | null => {\n  let oldest: Record<string, unknown> | null = null;\n  let oldestKey: string | null = null;\n\n  for (const record of records) {\n    const createdAt = record['createdAt'];\n    const key = typeof createdAt === 'string' ? createdAt : '';\n\n    if (oldest === null || oldestKey === null || key < oldestKey) {\n      oldest = record;\n      oldestKey = key;\n    }\n  }\n\n  return oldest;\n};\n\n/**\n * Structured log line. Logic functions have no logger injected \u2014 stdout is what\n * `yarn twenty dev:function:logs` shows \u2014 so everything Greenlight emits is a\n * single JSON object with a stable `event` key that support can grep for.\n */\nexport const logGreenlight = (\n  event: string,\n  detail: Record<string, unknown>,\n): void => {\n  // eslint-disable-next-line no-console\n  console.log(JSON.stringify({ app: 'numaya-greenlight', event, ...detail }));\n};\n\nexport const describeError = (error: unknown): string => {\n  if (error instanceof Error) {\n    return `${error.name}: ${error.message}`;\n  }\n\n  if (typeof error === 'string') {\n    return error;\n  }\n\n  try {\n    return JSON.stringify(error) ?? 'unknown error';\n  } catch {\n    return 'unknown error';\n  }\n};\n", "/**\n * The real licence store: Twenty's app key-value storage.\n *\n * Same shape and the same reasoning as `release-marker-store.ts` \u2014 this is the\n * only licensing module that imports the SDK, which is what lets `licence-run.ts`\n * and the whole of `src/licensing/` be driven by a fake in unit tests.\n *\n * `scope: 'WORKSPACE'` throughout, and it is not a detail. A licence belongs to\n * a workspace, its activation slot is claimed with the workspace id as the\n * fingerprint, and a read under a different scope silently finds nothing \u2014 which\n * would present as \"the cache never survives a restart\", i.e. as a permanent\n * grace-expired state that fails open forever and never says why.\n *\n * ## Reads are tolerant, writes are best-effort\n *\n * `kv.get` returns whatever was stored, which after an upgrade might be a shape\n * this version has never seen. Every read validates the couple of fields it\n * actually depends on and returns `null` / `unknown` otherwise, so a stale entry\n * degrades to \"no cache\" rather than to a `TypeError` inside a cron job.\n *\n * Writes swallow their errors for the same reason install-run's audit write\n * does: failing a licence check because the *cache* could not be written would\n * convert a storage blip into a lost validation, when the validation itself\n * already succeeded.\n */\n\nimport { kv } from 'twenty-sdk/logic-function';\n\nimport {\n  CALIBRATION_CACHE_KV_KEY,\n  CALIBRATION_STATE_KV_KEY,\n  LICENCE_ACTIVATION_KV_KEY,\n  LICENCE_CACHE_KV_KEY,\n  LICENCE_STATE_KV_KEY,\n} from 'src/constants/licence-identifiers';\nimport {\n  type CachedCalibration,\n  type CalibrationReaderPort,\n  type CalibrationStorePort,\n} from 'src/calibration/types';\nimport {\n  describeError,\n  logGreenlight,\n} from 'src/logic-functions/greenlight-api';\nimport {\n  type CachedLicenceValidation,\n  type LicenceActivationState,\n  type LicenceState,\n  type LicenceStorePort,\n} from 'src/licensing/types';\n\nconst SCOPE = { scope: 'WORKSPACE' } as const;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * A cache entry is only usable if it carries the three things the offline path\n * reads: when it was taken, and the two gate booleans. Everything else is\n * cosmetic and is allowed to be missing.\n */\nconst readCacheEntry = (value: unknown): CachedLicenceValidation | null => {\n  if (!isRecord(value)) {\n    return null;\n  }\n\n  const { cachedAt, valid, hasFeature } = value;\n\n  if (\n    typeof cachedAt !== 'string' ||\n    typeof valid !== 'boolean' ||\n    typeof hasFeature !== 'boolean'\n  ) {\n    return null;\n  }\n\n  return {\n    cachedAt,\n    maskedKey: typeof value['maskedKey'] === 'string' ? value['maskedKey'] : '',\n    valid,\n    hasFeature,\n    reason: typeof value['reason'] === 'string' ? value['reason'] : null,\n    features: Array.isArray(value['features'])\n      ? value['features'].filter((entry): entry is string => typeof entry === 'string')\n      : [],\n    tier: typeof value['tier'] === 'string' ? value['tier'] : null,\n    daysRemaining:\n      typeof value['daysRemaining'] === 'number' ? value['daysRemaining'] : null,\n    expiryWarning: value['expiryWarning'] === true,\n    expiryAt: typeof value['expiryAt'] === 'string' ? value['expiryAt'] : null,\n  };\n};\n\nconst readActivationState = (value: unknown): LicenceActivationState => {\n  if (!isRecord(value)) {\n    return { status: 'unknown' };\n  }\n\n  const status = value['status'];\n  const at = typeof value['at'] === 'string' ? value['at'] : '';\n\n  if (status === 'claimed' || status === 'limit_reached') {\n    return { status, at };\n  }\n\n  return { status: 'unknown' };\n};\n\nexport const kvLicenceStore: LicenceStorePort = {\n  readCache: async () => {\n    try {\n      return readCacheEntry(await kv.get<unknown>(LICENCE_CACHE_KV_KEY, SCOPE));\n    } catch (error) {\n      logGreenlight('licence_cache_read_failed', { error: describeError(error) });\n\n      return null;\n    }\n  },\n\n  writeCache: async (entry) => {\n    try {\n      await kv.set(LICENCE_CACHE_KV_KEY, entry, SCOPE);\n    } catch (error) {\n      logGreenlight('licence_cache_write_failed', { error: describeError(error) });\n    }\n  },\n\n  readActivation: async () => {\n    try {\n      return readActivationState(\n        await kv.get<unknown>(LICENCE_ACTIVATION_KV_KEY, SCOPE),\n      );\n    } catch (error) {\n      logGreenlight('licence_activation_read_failed', {\n        error: describeError(error),\n      });\n\n      return { status: 'unknown' };\n    }\n  },\n\n  writeActivation: async (state) => {\n    try {\n      await kv.set(LICENCE_ACTIVATION_KV_KEY, state, SCOPE);\n    } catch (error) {\n      logGreenlight('licence_activation_write_failed', {\n        error: describeError(error),\n      });\n    }\n  },\n\n  publishState: async (state) => {\n    try {\n      await kv.set(LICENCE_STATE_KV_KEY, state, SCOPE);\n    } catch (error) {\n      logGreenlight('licence_state_publish_failed', { error: describeError(error) });\n    }\n  },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Calibration                                                                 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A cached calibration is only usable if it carries when it was taken and a\n * payload with a version. Everything else is allowed to be missing and is\n * defaulted, for the same reason the entitlement cache is read tolerantly: an\n * entry written by a previous release must degrade to \"no calibration\" \u2014 i.e.\n * to shipped defaults \u2014 rather than to a `TypeError` inside a cron job.\n *\n * The lists are *not* re-validated element by element here. They were validated\n * and normalised by `src/calibration/parse.ts` before the signature was checked,\n * and re-deriving that on every read would spend a scoring run's time to\n * re-establish a fact the write already established.\n */\nconst readCalibrationEntry = (value: unknown): CachedCalibration | null => {\n  if (!isRecord(value)) {\n    return null;\n  }\n\n  const { cachedAt, calibration, sealTag, verifiedAt, keyId } = value;\n\n  if (typeof cachedAt !== 'string' || !isRecord(calibration)) {\n    return null;\n  }\n\n  if (typeof calibration['calibrationVersion'] !== 'number') {\n    return null;\n  }\n\n  // Every field the seal covers is read strictly, with no defaulting. A\n  // defaulted value would change the canonical bytes the tag is checked against\n  // and turn a legitimate entry into a seal mismatch \u2014 the failure would look\n  // like tampering, which is the most misleading thing it could look like. An\n  // entry missing any of them is simply not a sealed entry.\n  if (\n    typeof sealTag !== 'string' ||\n    sealTag.length === 0 ||\n    typeof verifiedAt !== 'string' ||\n    typeof keyId !== 'string'\n  ) {\n    return null;\n  }\n\n  return {\n    cachedAt,\n    verifiedAt,\n    keyId,\n    sealTag,\n    calibration: calibration as unknown as CachedCalibration['calibration'],\n  };\n};\n\nexport const kvCalibrationStore: CalibrationStorePort = {\n  readCalibration: async () => {\n    try {\n      return readCalibrationEntry(\n        await kv.get<unknown>(CALIBRATION_CACHE_KV_KEY, SCOPE),\n      );\n    } catch (error) {\n      logGreenlight('calibration_cache_read_failed', {\n        error: describeError(error),\n      });\n\n      return null;\n    }\n  },\n\n  writeCalibration: async (entry) => {\n    try {\n      await kv.set(CALIBRATION_CACHE_KV_KEY, entry, SCOPE);\n    } catch (error) {\n      logGreenlight('calibration_cache_write_failed', {\n        error: describeError(error),\n      });\n    }\n  },\n\n  clearCalibration: async () => {\n    try {\n      // `kv.set(key, null)` rather than a delete: the SDK's key-value surface\n      // has no delete, and a stored `null` fails `readCalibrationEntry`'s record\n      // check, which is exactly \"no calibration\". Writing an empty object would\n      // not \u2014 it would parse as a record and then fail on a missing field, which\n      // is the same outcome by a longer route and one more shape to reason about.\n      await kv.set(CALIBRATION_CACHE_KV_KEY, null, SCOPE);\n    } catch (error) {\n      logGreenlight('calibration_cache_clear_failed', {\n        error: describeError(error),\n      });\n    }\n  },\n\n  publishCalibrationState: async (state) => {\n    try {\n      await kv.set(CALIBRATION_STATE_KV_KEY, state, SCOPE);\n    } catch (error) {\n      logGreenlight('calibration_state_publish_failed', {\n        error: describeError(error),\n      });\n    }\n  },\n};\n\n/**\n * The scoring path's view: read-only, and narrower than the store above.\n *\n * A scoring run must not be handed something it could write through \u2014 the same\n * argument that keeps `readPublishedLicenceState` off `LicenceStorePort`. A run\n * that could rewrite the calibration cache would be a run that could change what\n * every *subsequent* lead is scored against, from inside a per-lead code path.\n */\nexport const kvCalibrationReader: CalibrationReaderPort = {\n  read: () => kvCalibrationStore.readCalibration(),\n};\n\n/**\n * Read the published calibration state back, for the admin panel.\n *\n * Returns `null` rather than a synthetic \"shipped defaults\" state when nothing\n * has been published: \"this workspace has never run a licence check\" and \"this\n * workspace ran one and is on shipped defaults\" are different things to show a\n * human, and inventing the second would hide the first.\n */\nexport const readPublishedCalibrationState = async (): Promise<unknown> => {\n  try {\n    const value = await kv.get<unknown>(CALIBRATION_STATE_KV_KEY, SCOPE);\n\n    return isRecord(value) && typeof value['status'] === 'string' ? value : null;\n  } catch (error) {\n    logGreenlight('calibration_state_read_failed', {\n      error: describeError(error),\n    });\n\n    return null;\n  }\n};\n\n/**\n * Read the published state back \u2014 what a future enrichment path calls before\n * consulting `isEnrichmentEnabled`. Deliberately not on `LicenceStorePort`: the\n * port is the *writer's* interface, and enrichment has no business being handed\n * something it could write through.\n */\nexport const readPublishedLicenceState = async (): Promise<LicenceState | null> => {\n  try {\n    const value = await kv.get<unknown>(LICENCE_STATE_KV_KEY, SCOPE);\n\n    if (!isRecord(value) || typeof value['mode'] !== 'string') {\n      return null;\n    }\n\n    return value as unknown as LicenceState;\n  } catch (error) {\n    logGreenlight('licence_state_read_failed', { error: describeError(error) });\n\n    return null;\n  }\n};\n", "/**\n * Cache freshness and offline-grace arithmetic.\n *\n * Pure: `now` is passed in, never read. The scoring engine holds the same line\n * for the same reason \u2014 a clock read inside a decision function makes the\n * decision untestable at its boundaries, and every interesting case here *is* a\n * boundary.\n *\n * ---------------------------------------------------------------------------\n * ## The windows, and why they are these windows\n *\n * From the error-state table in `LICENSING_INTEGRATION.md`:\n *\n * | Cache age        | Behaviour                                     |\n * |------------------|-----------------------------------------------|\n * | `< 24h`          | Cached entitlement used as-is                 |\n * | `24h \u2013 72h`      | Rules-only, \"grace period\"                    |\n * | `> 72h`          | Rules-only + visible warning                  |\n *\n * Note that grace **ends at 72h from `cachedAt`**, not at 24h + 72h. The prose\n * elsewhere reads \"24h cache, then a 72h offline grace window\", which would put\n * the cliff at 96h; the table is the specific claim and the table is what is\n * implemented. The difference is 24 hours of degraded-but-warned operation and\n * it changes nothing about safety, because **nothing is ever blocked at any\n * point on this ladder** \u2014 past 72h the product is still scoring leads, it is\n * just scoring them without enrichment and saying so loudly.\n *\n * ## Why grace disables enrichment rather than extending it\n *\n * Because the two failure modes are not symmetric. Trusting a stale entitlement\n * for three days means potentially serving a paid feature to a revoked licence,\n * and revocation is the one lever that has to work. Turning enrichment off means\n * the customer loses an optional enhancement during a Numaya outage. The second\n * is recoverable in seconds when the service returns; the first is not\n * recoverable at all.\n *\n * The 24h band is the exception, and it is bounded precisely so that \"Numaya\n * restarted a container\" never costs a customer anything.\n * ---------------------------------------------------------------------------\n */\n\nconst HOUR_MS = 60 * 60 * 1000;\n\n/** Entitlements younger than this are used exactly as if they were live. */\nexport const LICENCE_CACHE_TTL_MS = 24 * HOUR_MS;\n\n/**\n * Age at which the offline grace window closes, measured from `cachedAt`.\n * Past this the product warns visibly \u2014 it does not stop.\n */\nexport const LICENCE_OFFLINE_GRACE_LIMIT_MS = 72 * HOUR_MS;\n\nexport type CacheFreshness = 'fresh' | 'grace' | 'expired';\n\n/**\n * Age of a cached entitlement in milliseconds, or `null` when `cachedAt` is\n * missing or unparseable.\n *\n * A negative age \u2014 a cache written by a machine whose clock is ahead \u2014 is\n * clamped to `0` rather than rejected. The alternative is that a small clock\n * skew on the licensing service makes a workspace look like it has never\n * validated, which is a worse outcome than briefly treating a slightly-future\n * entitlement as fresh.\n */\nexport const cacheAgeMs = (\n  cachedAt: string | null | undefined,\n  now: Date,\n): number | null => {\n  if (typeof cachedAt !== 'string' || cachedAt.length === 0) {\n    return null;\n  }\n\n  const cachedMs = Date.parse(cachedAt);\n\n  if (Number.isNaN(cachedMs)) {\n    return null;\n  }\n\n  return Math.max(0, now.getTime() - cachedMs);\n};\n\n/**\n * Boundaries are inclusive at the *lower* edge of the worse band: an entitlement\n * exactly 24h old is already in grace, and one exactly 72h old is already\n * expired. Erring towards the stricter band on the boundary keeps the rule\n * \"fresh means strictly under a day old\" true as stated.\n */\nexport const classifyCacheAge = (ageMs: number): CacheFreshness => {\n  if (ageMs < LICENCE_CACHE_TTL_MS) {\n    return 'fresh';\n  }\n\n  if (ageMs < LICENCE_OFFLINE_GRACE_LIMIT_MS) {\n    return 'grace';\n  }\n\n  return 'expired';\n};\n\n/**\n * `null` when there is no usable cache at all \u2014 which is not the same as an\n * expired one, and the admin notice says so differently.\n */\nexport const classifyCache = (\n  cachedAt: string | null | undefined,\n  now: Date,\n): { readonly freshness: CacheFreshness | null; readonly ageMs: number | null } => {\n  const ageMs = cacheAgeMs(cachedAt, now);\n\n  return {\n    freshness: ageMs === null ? null : classifyCacheAge(ageMs),\n    ageMs,\n  };\n};\n", "/**\n * Resolving \"am I running shipped defaults or calibration vNN, and when did it\n * last refresh?\" \u2014 one pure function, `now` injected, no I/O.\n *\n * ===========================================================================\n * ## The same ladder as the entitlement, for the same reasons\n *\n * Calibration is cached beside the entitlement in workspace key-value storage\n * and walks the identical 24h / 72h freshness ladder from\n * `src/licensing/cache.ts`. Sharing the arithmetic is not laziness \u2014 the two\n * caches are refreshed by the *same nightly run*, so any second ladder would\n * differ from this one only by drifting out of step with it.\n *\n * Where the two differ is what the far end of the ladder means, and the\n * difference is the whole point:\n *\n *   | Age        | Entitlement                  | Calibration                 |\n *   |------------|------------------------------|-----------------------------|\n *   | `< 24h`    | used as-is                   | used as-is                  |\n *   | `24\u201372h`   | rules-only (\"grace\")         | **still used**              |\n *   | `> 72h`    | rules-only + loud warning    | shipped defaults            |\n *\n * The entitlement tightens at 24h because trusting a stale one risks serving a\n * paid feature to a revoked licence, and revocation has to work. Calibration\n * carries no such risk: it is a word list. Dropping it the moment Numaya has a\n * bad night would make a customer's scores move for a reason that has nothing to\n * do with their data, which is a worse outcome than a slightly old vocabulary.\n * So calibration survives the grace window and is only abandoned past 72h, at\n * which point \"we have not spoken to Numaya in three days\" is a real statement\n * about the install and the admin should be seeing shipped-default behaviour\n * they can reason about.\n *\n * Note the asymmetry is safe in both directions: past 72h calibration falls back\n * to the shipped defaults, which is a *working product*, not a degraded one.\n * There is no branch below that can stop a lead being scored.\n * ===========================================================================\n */\n\nimport { classifyCache } from 'src/licensing/cache';\nimport {\n  type CachedCalibration,\n  type Calibration,\n  type CalibrationCounts,\n  type CalibrationReason,\n  type CalibrationState,\n} from 'src/calibration/types';\n\nexport const countCalibration = (\n  calibration: Calibration,\n): CalibrationCounts => ({\n  decisionMakerTitles: calibration.decisionMakerTitles.length,\n  influencerTitles: calibration.influencerTitles.length,\n  roleInboxLocalParts: calibration.roleInboxLocalParts.length,\n  placeholderValues: calibration.placeholderValues.length,\n  industrySynonyms: Object.keys(calibration.industrySynonyms).length,\n});\n\n/**\n * The state to publish when no usable calibration exists.\n *\n * Every field that describes a payload is `null` rather than zero or empty\n * string: \"there is no calibration\" and \"there is a calibration with nothing in\n * it\" must not render the same in the admin panel, and a zero count is exactly\n * how the second one would look.\n */\nexport const shippedDefaultsState = (\n  reason: CalibrationReason,\n  now: Date,\n): CalibrationState => ({\n  status: 'shipped_defaults',\n  reason,\n  source: 'none',\n  calibrationVersion: null,\n  issuedAt: null,\n  refreshedAt: null,\n  checkedAt: now.toISOString(),\n  cacheAgeMs: null,\n  keyId: null,\n  counts: null,\n});\n\nexport const calibratedState = (\n  entry: CachedCalibration,\n  source: 'live' | 'cache',\n  cacheAgeMs: number | null,\n  now: Date,\n): CalibrationState => ({\n  status: 'calibrated',\n  reason: 'calibrated',\n  source,\n  calibrationVersion: entry.calibration.calibrationVersion,\n  issuedAt: entry.calibration.issuedAt.length > 0 ? entry.calibration.issuedAt : null,\n  refreshedAt: entry.verifiedAt,\n  checkedAt: now.toISOString(),\n  cacheAgeMs,\n  keyId: entry.keyId,\n  counts: countCalibration(entry.calibration),\n});\n\n/**\n * Age at which a cached calibration stops being used. Deliberately the\n * entitlement's *outer* limit, not its inner one \u2014 see the table above.\n */\nexport const CALIBRATION_MAX_AGE_MS = 72 * 60 * 60 * 1000;\n\nexport interface ResolveCalibrationInput {\n  /** The cached payload, or `null` when none has ever been stored. */\n  readonly cached: CachedCalibration | null;\n  readonly now: Date;\n}\n\nexport interface ResolvedCalibration {\n  readonly state: CalibrationState;\n  /** The payload the scoring path should use, or `null` for shipped defaults. */\n  readonly calibration: Calibration | null;\n}\n\n/**\n * What the scoring path should use right now, and what the admin should be told.\n *\n * ===========================================================================\n * ## Why there is no entitlement check here\n *\n * Because the *existence of the cache is* the entitlement check, and moving it\n * to the writer is what keeps a licence lookup out of the per-lead scoring path.\n *\n * `refreshCalibration` writes the cache only while the licence resolves to\n * `full`, and **deletes it** the moment a nightly run finds the entitlement\n * gone. So a present, fresh cache already means \"this workspace was entitled at\n * its last successful licence check\". Re-deriving that per lead would mean a\n * second key-value read on the hot path to re-establish a fact the nightly run\n * has already written down.\n *\n * The consequence is a revocation latency of at most 24 hours \u2014 one nightly\n * cycle \u2014 which is exactly the latency enrichment revocation already has and is\n * documented as having, for the same reason: the alternative is putting a\n * network call to Numaya in front of a customer's scoring path, which is the\n * coupling the fail-open rule exists to prevent.\n *\n * The reverse case costs the customer one night. A renewed licence re-fetches on\n * the next run, and until then the workspace scores on shipped defaults \u2014 the\n * full working product, exactly as an unlicensed install always has.\n * ===========================================================================\n */\nexport const resolveCalibration = (\n  input: ResolveCalibrationInput,\n): ResolvedCalibration => {\n  if (input.cached === null) {\n    return {\n      state: shippedDefaultsState('not_provisioned', input.now),\n      calibration: null,\n    };\n  }\n\n  const { ageMs } = classifyCache(input.cached.cachedAt, input.now);\n\n  // An unreadable `cachedAt` is treated as expired rather than as fresh. The\n  // opposite choice would make a corrupt timestamp mean \"trust this forever\",\n  // which is the one thing a freshness check must never be talked into.\n  if (ageMs === null || ageMs >= CALIBRATION_MAX_AGE_MS) {\n    return {\n      state: {\n        ...shippedDefaultsState('stale', input.now),\n        cacheAgeMs: ageMs,\n        calibrationVersion: input.cached.calibration.calibrationVersion,\n        keyId: input.cached.keyId,\n      },\n      calibration: null,\n    };\n  }\n\n  return {\n    state: calibratedState(\n      input.cached,\n      ageMs === 0 ? 'live' : 'cache',\n      ageMs,\n      input.now,\n    ),\n    calibration: input.cached.calibration,\n  };\n};\n", "/**\n * The enrichable field catalogue: what may be asked for, in what shape.\n *\n * One table, read by three consumers that must not disagree \u2014 the prompt (what\n * the model is asked), the extractor (what is accepted back) and the gap\n * analysis (what is worth asking about at all). Keeping them keyed off the same\n * array is what stops the classic drift where a field is prompted for and then\n * silently discarded, or accepted without ever being requested.\n *\n * Every entry answers a *firmographic* question. Nothing here is a contact\n * detail: see the note on `EnrichableFieldKey` for why that boundary is a hard\n * one rather than a starting point.\n */\n\nimport type {\n  EnrichableFieldKey,\n  EnrichableFieldSpec,\n} from 'src/enrichment/types';\n\nexport const ENRICHABLE_FIELD_SPECS: readonly EnrichableFieldSpec[] = [\n  {\n    key: 'industry',\n    label: 'Industry',\n    question: \"What industry does this person's employer operate in?\",\n    kind: 'text',\n    maxLength: 60,\n  },\n  {\n    key: 'region',\n    label: 'Region',\n    question: 'Which country or region is the employer headquartered in?',\n    kind: 'text',\n    maxLength: 60,\n  },\n  {\n    key: 'employeeCount',\n    label: 'Employee count',\n    question: 'Approximately how many people does the employer employ?',\n    kind: 'integer',\n    maxLength: 12,\n  },\n  {\n    key: 'jobTitle',\n    label: 'Job title',\n    question: 'What is this person\u2019s job title at that employer?',\n    kind: 'text',\n    maxLength: 120,\n  },\n  {\n    key: 'seniority',\n    label: 'Seniority',\n    question:\n      'What seniority level does that job title correspond to (for example: C-level, VP, Director, Manager, Individual contributor)?',\n    kind: 'text',\n    maxLength: 40,\n  },\n];\n\nconst SPEC_BY_KEY: ReadonlyMap<string, EnrichableFieldSpec> = new Map(\n  ENRICHABLE_FIELD_SPECS.map((spec) => [spec.key, spec]),\n);\n\nexport const findFieldSpec = (key: string): EnrichableFieldSpec | null =>\n  SPEC_BY_KEY.get(key) ?? null;\n\nexport const isEnrichableFieldKey = (key: unknown): key is EnrichableFieldKey =>\n  typeof key === 'string' && SPEC_BY_KEY.has(key);\n\n/**\n * The dotted paths a workspace adds to its Layer 3 field mapping to let the\n * deterministic scorer read enriched values.\n *\n * Published from here rather than hard-coded in the config seed because the\n * shape of the provenance blob is this module's business, and a path written out\n * by hand somewhere else is a path that goes stale the first time the blob's\n * version changes.\n *\n * The ordering is the important part: the enriched path goes **after** the\n * workspace's own candidates in every mapping, never before. `field-access.ts`\n * tries candidates in order and takes the first that yields a value, so a human\n * value always wins and enrichment only ever fills a hole. That is the same\n * promise the storage shape makes \u2014 enrichment never overwrites a person \u2014 held\n * at the read side as well as the write side.\n */\nexport const enrichedFieldMappingPath = (key: EnrichableFieldKey): string =>\n  `greenlightEnrichment.fields.${key}.parsedValue`;\n\nexport const ENRICHED_FIELD_MAPPING_PATHS: Readonly<\n  Record<EnrichableFieldKey, string>\n> = Object.freeze(\n  Object.fromEntries(\n    ENRICHABLE_FIELD_SPECS.map((spec) => [\n      spec.key,\n      enrichedFieldMappingPath(spec.key),\n    ]),\n  ) as Record<EnrichableFieldKey, string>,\n);\n", "/**\n * The licensing composition root: reads the environment, drives the ports, hands\n * everything to the pure core, writes the result back out.\n *\n * Kept out of the `define*` file for the same reason `scoring-run.ts` is \u2014 so\n * the whole run can be exercised against fakes, with an injected `now` and no\n * network. There is no `new Date()` and no `new CoreApiClient()` below.\n *\n * ===========================================================================\n * ## Two entry points\n *\n * `runLicenceInstall`  \u2014 called once from the post-install hook. Activates, then\n *                        validates.\n * `runLicenceRevalidation` \u2014 called nightly by the cron function. Validates only.\n *\n * `activate` is **not** called nightly. It is idempotent on the fingerprint and\n * would be harmless, but it is a slot-claim, not a health check, and calling it\n * every night would turn one write per install into one write per workspace per\n * day against a service that gains nothing from them.\n *\n * ## What is sent to Numaya, and what is not\n *\n * The licence key, the workspace id (as `deviceFingerprint`) and the workspace\n * display name (as `deviceName`). That is the entire payload, and the data-egress\n * table in `ARCHITECTURE.md` says so. No lead, contact, company or score data\n * leaves the workspace on this path \u2014 there is no field in the request body that\n * could carry any.\n *\n * ## Why an unresolvable workspace is not fatal\n *\n * `deviceFingerprint` is optional on `validate`. If the workspace identity\n * cannot be read \u2014 the metadata API is unavailable, or a future SDK moves\n * `currentWorkspace` \u2014 the run still validates, just without a fingerprint, and\n * skips activation because activation *requires* one. That is a strictly better\n * outcome than failing the check: the customer keeps their entitlement, and the\n * only thing lost is the readable device name in Numaya's admin view.\n * ===========================================================================\n */\n\nimport {\n  DEFAULT_LICENCE_API_BASE_URL,\n  DEFAULT_LICENCE_ENVIRONMENT,\n  ENRICHMENT_FEATURE_NAME,\n} from 'src/constants/licence-identifiers';\nimport {\n  buildLicenceAuditDetail,\n  buildLicenceAuditRow,\n  describeLicenceForAdmin,\n  maskLicenceKey,\n  redactLicenceKey,\n  resolveLicenceState,\n  type LicenceNotice,\n  type LicenceEnvironmentName,\n  type LicenceState,\n  type LicenceStorePort,\n  type LicensingPort,\n  type LicenceValidateOutcome,\n} from 'src/licensing';\nimport {\n  describeCalibration,\n  refreshCalibration,\n  type CalibrationStorePort,\n  type RefreshCalibrationOutcome,\n} from 'src/calibration';\nimport {\n  describeError,\n  logGreenlight,\n  type GreenlightApiClient,\n} from 'src/logic-functions/greenlight-api';\nimport { createLicenceKeySeal } from 'src/logic-functions/licence-cache-seal';\n\n/* -------------------------------------------------------------------------- */\n/* Ports                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface WorkspaceIdentity {\n  readonly workspaceId: string;\n  readonly workspaceName: string | null;\n}\n\n/**\n * Resolving \"which workspace am I\" is I/O, so it is a port like everything else.\n * The real adapter is `licence-workspace-identity.ts`; `null` means \"could not\n * tell\", which every caller below treats as a degradation rather than a failure.\n */\nexport interface WorkspaceIdentityPort {\n  read(): Promise<WorkspaceIdentity | null>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Environment                                                                 */\n/* -------------------------------------------------------------------------- */\n\nexport interface LicenceEnvironment {\n  /** `null` when unset or blank \u2014 the supported, unlicensed configuration. */\n  readonly licenceKey: string | null;\n  readonly baseUrl: string;\n  readonly environment: LicenceEnvironmentName;\n}\n\n/**\n * Application variables reach a logic function as environment variables.\n *\n * The `?? DEFAULT_LICENCE_API_BASE_URL` is belt-and-braces: the variable ships\n * with that value declared in `application-config.ts`, so it should always be\n * present. It would be absent on a workspace that installed a build predating\n * the variable and has not re-synced, and defaulting beats an empty base URL\n * only in that the failure is then identical to a normal outage.\n *\n * `LICENCE_ENVIRONMENT` is read the strict way round: **only** the exact string\n * `sandbox` selects sandbox, and everything else \u2014 unset, blank, misspelled,\n * `SANDBOX_`, a value from a future release \u2014 is production. Defaulting an\n * unrecognised value to sandbox would let a typo route a paying customer at the\n * test estate, where their licence does not exist and where this whole exercise\n * started. Casing and surrounding whitespace are forgiven because they are\n * transcription noise, not intent.\n */\nexport const readLicenceEnvironment = (\n  env: Record<string, string | undefined> = process.env,\n): LicenceEnvironment => {\n  const rawKey = env['LICENCE_KEY'];\n  const trimmed = typeof rawKey === 'string' ? rawKey.trim() : '';\n  const rawBase = env['LICENCE_API_BASE_URL'];\n  const rawEnvironment = env['LICENCE_ENVIRONMENT'];\n\n  return {\n    licenceKey: trimmed.length === 0 ? null : trimmed,\n    baseUrl:\n      typeof rawBase === 'string' && rawBase.trim().length > 0\n        ? rawBase.trim()\n        : DEFAULT_LICENCE_API_BASE_URL,\n    environment:\n      typeof rawEnvironment === 'string' &&\n      rawEnvironment.trim().toLowerCase() === 'sandbox'\n        ? 'sandbox'\n        : DEFAULT_LICENCE_ENVIRONMENT,\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runs                                                                        */\n/* -------------------------------------------------------------------------- */\n\nexport interface LicenceRunDeps {\n  readonly licensing: LicensingPort;\n  readonly store: LicenceStorePort;\n  /**\n   * Where the verified calibration payload is cached and its state published.\n   *\n   * Optional, and its absence is a supported configuration rather than a\n   * mistake: a run without it resolves the entitlement exactly as it always\n   * did and simply does no calibration work. That is what makes this an\n   * additive change to a path whose failure is silent and nightly.\n   */\n  readonly calibration?: CalibrationStorePort | null;\n  readonly identity: WorkspaceIdentityPort;\n  /** Optional: without it the run still resolves state, it just writes no row. */\n  readonly client?: GreenlightApiClient | null;\n  readonly licenceKey: string | null;\n  /**\n   * Which environment the `licensing` port was pointed at. Passed separately\n   * rather than read back off the port because the port is an interface with no\n   * way to ask, and because the resolved state has to record what we *asked\n   * for* even when the call never left the building.\n   */\n  readonly environment: LicenceEnvironmentName;\n  readonly now: Date;\n}\n\nexport interface LicenceRunOutcome {\n  readonly state: LicenceState;\n  readonly notice: LicenceNotice;\n  /** What the activation attempt did, for the install path's return value. */\n  readonly activation: 'claimed' | 'limit_reached' | 'skipped' | 'failed' | 'not_attempted';\n  /** Null when no calibration store was wired. */\n  readonly calibration: RefreshCalibrationOutcome | null;\n}\n\n/**\n * Best-effort provenance row. Never fails the run \u2014 a licence check that\n * succeeded but could not be written to the audit log is still a licence check\n * that succeeded, and the resolved state has already been published to key-value\n * storage where the UI reads it.\n */\nconst writeLicenceAuditRow = async (\n  client: GreenlightApiClient | null | undefined,\n  state: LicenceState,\n  calibration: RefreshCalibrationOutcome | null,\n): Promise<boolean> => {\n  if (client === null || client === undefined) {\n    return false;\n  }\n\n  try {\n    await client.mutation({\n      createGreenlightAuditLog: {\n        __args: {\n          data: buildLicenceAuditRow(state, calibration?.state ?? null),\n        },\n        id: true,\n      },\n    });\n\n    return true;\n  } catch (error) {\n    logGreenlight('licence_audit_write_failed', { error: describeError(error) });\n\n    return false;\n  }\n};\n\n/**\n * Resolve, persist, publish, audit, log. Shared by both entry points so the\n * install path and the nightly path cannot drift.\n */\nconst finishRun = async (\n  deps: LicenceRunDeps,\n  outcome: LicenceValidateOutcome | null,\n  activation: LicenceRunOutcome['activation'],\n): Promise<LicenceRunOutcome> => {\n  const presence = {\n    hasKey: deps.licenceKey !== null,\n    maskedKey: maskLicenceKey(deps.licenceKey),\n  };\n\n  const [cached, activationState] = await Promise.all([\n    deps.store.readCache(),\n    deps.store.readActivation(),\n  ]);\n\n  const { state, cacheWrite } = resolveLicenceState({\n    presence,\n    outcome,\n    cached,\n    activation: activationState,\n    environment: deps.environment,\n    now: deps.now,\n  });\n\n  if (cacheWrite !== null) {\n    await deps.store.writeCache(cacheWrite);\n  }\n\n  await deps.store.publishState(state);\n\n  // Calibration runs strictly after the entitlement has been resolved and\n  // published, and never before. See `src/calibration/refresh.ts`, \"Ordering\":\n  // nothing about a word list may be able to delay, fail or precede the decision\n  // that gates the paid feature and writes the admin notice.\n  const calibration =\n    deps.calibration === null || deps.calibration === undefined\n      ? null\n      : await refreshCalibration({\n          store: deps.calibration,\n          // Built here because this is one of the two modules that legitimately\n          // holds the raw key. The seal itself never receives it \u2014 see\n          // `licence-cache-seal.ts`.\n          seal: createLicenceKeySeal(deps.licenceKey),\n          metadata:\n            outcome !== null && outcome.kind === 'validated'\n              ? outcome.response.customerMetadata\n              : undefined,\n          live: outcome !== null && outcome.kind === 'validated',\n          // The same entitlement that gates enrichment. Calibration is the other\n          // half of what a licence buys, so it is bought on the same terms.\n          entitled: state.mode === 'full',\n          now: deps.now,\n        });\n\n  // Contains the mask, never the key \u2014 `buildLicenceAuditDetail` reads only\n  // fields of `LicenceState`, and `LicenceState` has no field that could hold\n  // a key. `describeCalibration` reads only counts and version numbers, never\n  // payload content.\n  logGreenlight('licence_resolved', {\n    ...buildLicenceAuditDetail(state),\n    activationAttempt: activation,\n    ...(calibration === null ? {} : describeCalibration(calibration)),\n  });\n\n  await writeLicenceAuditRow(deps.client, state, calibration);\n\n  return {\n    state,\n    notice: describeLicenceForAdmin(state),\n    activation,\n    calibration,\n  };\n};\n\nconst validateOnce = async (\n  deps: LicenceRunDeps,\n  workspace: WorkspaceIdentity | null,\n): Promise<LicenceValidateOutcome | null> => {\n  if (deps.licenceKey === null) {\n    return null;\n  }\n\n  try {\n    return await deps.licensing.validate({\n      key: deps.licenceKey,\n      deviceFingerprint: workspace?.workspaceId ?? null,\n      feature: ENRICHMENT_FEATURE_NAME,\n    });\n  } catch (error) {\n    // `LicensingPort` is documented as never throwing and the shipped adapter\n    // honours it. This catch is here because \"documented\" is not \"enforced by\n    // the type system\": a future adapter, or a test double, can throw, and the\n    // consequence would be a cron run that dies before publishing state.\n    // Downgrading it to the outcome the ladder already handles costs three\n    // lines and removes the failure mode entirely.\n    return {\n      kind: 'transport_failure',\n      detail: redactLicenceKey(describeError(error), deps.licenceKey),\n    };\n  }\n};\n\nconst readWorkspace = async (\n  identity: WorkspaceIdentityPort,\n): Promise<WorkspaceIdentity | null> => {\n  try {\n    return await identity.read();\n  } catch (error) {\n    logGreenlight('licence_workspace_identity_failed', {\n      error: describeError(error),\n    });\n\n    return null;\n  }\n};\n\n/**\n * Install: claim the activation slot, then validate.\n *\n * ## Why it validates even after a `409`\n *\n * The integration note's sketch returns early on `409`. This does not, and the\n * difference matters: because `validate` does not consult activation slots for\n * `Trial` / `Subscription` licences (gotcha 2), a refused workspace still gets\n * `valid: true, hasFeature: true`. Returning early would hide that, and \u2014 worse\n * \u2014 the *nightly* run has no activation step at all, so it would quietly switch\n * enrichment on the following morning.\n *\n * Instead the `409` is persisted, and `resolveLicenceState` applies it as an\n * override on every subsequent resolution. Validating anyway is then free\n * information: the admin notice can say how many days are left on the licence\n * that another workspace is holding, which is exactly what they need to decide\n * whether to free a slot or buy a seat.\n *\n * ## Idempotency\n *\n * `activate` is idempotent on `deviceFingerprint` \u2014 a repeat call with the same\n * workspace id returns the existing activation and consumes no second slot.\n * Reinstalling is therefore safe, and a successful activate clears a previously\n * stored `limit_reached`, which is how an admin who freed a slot recovers.\n */\nexport const runLicenceInstall = async (\n  deps: LicenceRunDeps,\n): Promise<LicenceRunOutcome> => {\n  if (deps.licenceKey === null) {\n    logGreenlight('licence_install_skipped', { reason: 'no_licence_key' });\n\n    return finishRun(deps, null, 'not_attempted');\n  }\n\n  const workspace = await readWorkspace(deps.identity);\n  let activation: LicenceRunOutcome['activation'] = 'skipped';\n\n  if (workspace === null) {\n    logGreenlight('licence_activate_skipped', {\n      reason: 'workspace_identity_unavailable',\n    });\n  } else {\n    const result = await deps.licensing.activate({\n      key: deps.licenceKey,\n      deviceFingerprint: workspace.workspaceId,\n      deviceName: workspace.workspaceName,\n    });\n\n    if (result.kind === 'activated') {\n      activation = 'claimed';\n      await deps.store.writeActivation({\n        status: 'claimed',\n        at: deps.now.toISOString(),\n      });\n    } else if (result.kind === 'activation_limit_reached') {\n      activation = 'limit_reached';\n      await deps.store.writeActivation({\n        status: 'limit_reached',\n        at: deps.now.toISOString(),\n      });\n    } else {\n      // A transport failure tells us nothing about the slot, so the stored\n      // activation state is left exactly as it was. Overwriting a known\n      // `claimed` with `unknown` because the network blipped would be losing\n      // information, not recording it.\n      activation = 'failed';\n      logGreenlight('licence_activate_failed', { failureKind: result.kind });\n    }\n  }\n\n  const outcome = await validateOnce(deps, workspace);\n\n  return finishRun(deps, outcome, activation);\n};\n\n/**\n * Nightly: validate, refresh the cache, republish the state, write the row.\n *\n * Never throws. The cron handler has nowhere to report a thrown error to, and a\n * run that dies before `finishRun` is a run that leaves yesterday's state\n * published \u2014 which is fine for a night and wrong for a month.\n */\nexport const runLicenceRevalidation = async (\n  deps: LicenceRunDeps,\n): Promise<LicenceRunOutcome> => {\n  const workspace = deps.licenceKey === null ? null : await readWorkspace(deps.identity);\n  const outcome = await validateOnce(deps, workspace);\n\n  return finishRun(deps, outcome, 'not_attempted');\n};\n", "import { defineLogicFunction } from 'twenty-sdk/define';\nimport { Response } from 'twenty-sdk/logic-function';\n\nimport {\n  LICENCE_PURCHASE_URL,\n  LICENCE_ROUTE_PATH,\n  LICENCE_STATUS_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,\n} from 'src/constants/licence-identifiers';\nimport { buildLicencePanelView } from 'src/licensing/panel';\nimport { readPublishedLicenceState } from 'src/logic-functions/licence-cache-store';\nimport { readLicenceEnvironment } from 'src/logic-functions/licence-run';\n\n/**\n * What licence is this workspace running, and how does an admin change it.\n *\n * ## Why this reads and never validates\n *\n * The obvious design is a route that calls the licensing service and reports\n * what it says. It is the wrong one twice over. It would put a network call to\n * Numaya on a path a human triggers, so the panel would hang or lie whenever we\n * are slow \u2014 and the nightly run already resolves exactly this state, applies\n * the cache and grace ladder to it, and publishes the result. A second resolver\n * reachable from a button is a second answer that can disagree with the first,\n * and the one on the screen would win the argument in the customer's mind.\n *\n * So this returns the published state and says when it was taken. \"Checked 6\n * hours ago\" is a fact an admin can act on. A number that might be fresh and\n * might be a timeout is not.\n *\n * ## Why no write, and no key in the response\n *\n * The panel cannot set the key. `LICENCE_KEY` is an application variable and\n * Twenty already owns that surface, including keeping secrets out of the UI and\n * the logs \u2014 reimplementing it here would mean this route accepting a raw key\n * over HTTP and storing it somewhere, which is a worse version of a solved\n * problem. The response carries the *masked* key only, which is what the\n * published state already holds.\n */\nconst handler = async () => {\n  const { licenceKey } = readLicenceEnvironment();\n\n  const view = buildLicencePanelView({\n    state: await readPublishedLicenceState(),\n    // The presence of a key, never its value. A route that reads the variable\n    // to decide what to show must not be a route that can leak it.\n    hasKeyConfigured: licenceKey !== null,\n    purchaseUrl: LICENCE_PURCHASE_URL,\n  });\n\n  return new Response(view, { status: 200 });\n};\n\nexport default defineLogicFunction({\n  universalIdentifier: LICENCE_STATUS_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,\n  name: 'greenlight-licence-status',\n  description:\n    \"Reports Greenlight's current licence mode, what enrichment it does or does not include, and where to obtain a licence.\",\n  // One key-value read and no network call, so this is the cheapest route in the\n  // app. Ten seconds is well past what it needs and still short enough that a\n  // wedged read fails while the panel is open.\n  timeoutSeconds: 10,\n  handler,\n  httpRouteTriggerSettings: {\n    path: LICENCE_ROUTE_PATH,\n    httpMethod: 'POST',\n    isAuthRequired: true,\n  },\n});\n"],
  "mappings": "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",
  "names": ["s", "n", "e", "t", "t", "n", "r", "e", "t", "n", "cacheAgeMs"]
}
