@things-factory/auth-base 10.1.228 → 10.1.234
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/constants/error-code.d.ts +1 -1
- package/dist-server/constants/error-code.js +1 -1
- package/dist-server/constants/error-code.js.map +1 -1
- package/dist-server/controllers/signup.js +1 -1
- package/dist-server/controllers/signup.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/index.js +2 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/privilege/privilege-directive.d.ts +9 -0
- package/dist-server/service/privilege/privilege-directive.js +36 -14
- package/dist-server/service/privilege/privilege-directive.js.map +1 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.d.ts +6 -0
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js +9 -2
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/tests/declare-privileges.test.ts +29 -0
- package/tests/open-fields-framework.test.ts +1 -1
- package/tests/passkey-db.test.ts +9 -0
- package/tests/refusals-have-korean.test.ts +24 -0
- package/tests/signup-duplicate-db.test.ts +68 -0
- package/translations/en.json +5 -2
- package/translations/es.json +5 -2
- package/translations/ja.json +5 -2
- package/translations/ko.json +5 -2
- package/translations/ms.json +5 -2
- package/translations/zh.json +5 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.privilegeDirectiveResolver = exports.privilegeDirectiveTypeDefs = void 0;
|
|
4
|
+
exports.declarePrivileges = declarePrivileges;
|
|
4
5
|
exports.privilegeOfField = privilegeOfField;
|
|
5
6
|
const tslib_1 = require("tslib");
|
|
6
7
|
const graphql_1 = require("graphql");
|
|
@@ -15,6 +16,39 @@ const privilege_axis_js_1 = require("./privilege-axis.js");
|
|
|
15
16
|
const privilege_rejection_js_1 = require("./privilege-rejection.js");
|
|
16
17
|
const open_field_rule_js_1 = require("./open-field-rule.js");
|
|
17
18
|
process['PRIVILEGES'] = {};
|
|
19
|
+
/** The axis check every declaration passes, whoever declares it — `@privilege` or `declarePrivileges`. */
|
|
20
|
+
function checkAxis(where, privilege, how) {
|
|
21
|
+
/*
|
|
22
|
+
* The axis has to be one this installation declared.
|
|
23
|
+
*
|
|
24
|
+
* Without this, `privilege: "mutaiton"` registered a pair nobody would ever be granted
|
|
25
|
+
* — a gate shut forever, with no error to read. The list is not the framework's: see
|
|
26
|
+
* `privilege-axis.ts` for why, and for the fact that it must be declared at module
|
|
27
|
+
* import time rather than on a bootstrap hook.
|
|
28
|
+
*/
|
|
29
|
+
if (!(0, privilege_axis_js_1.isDeclaredAxis)(privilege)) {
|
|
30
|
+
throw new Error(`${how} on ${where} uses axis "${privilege}", which this installation does not declare. ` +
|
|
31
|
+
`Declared axes: ${[...(0, privilege_axis_js_1.declaredAxes)()].sort().join(', ')}. ` +
|
|
32
|
+
`Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +
|
|
33
|
+
`server/index.ts — a bootstrap hook runs after the schema is built and is too late.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Declares privileges a module asks in code (`checkPermission`) and no field asks with `@privilege` — a rule an issued
|
|
38
|
+
* document kind or a worklist step holds. The privilege sync creates rows only for what is declared, and a role task
|
|
39
|
+
* naming an undeclared pair is refused when a template is applied (ADR-0176, 2026-10-30: `employee-certificate` was
|
|
40
|
+
* asked in code only, and `office.finance` could not be applied).
|
|
41
|
+
*
|
|
42
|
+
* Call it at the top level of the file that asks them, as `declareAxes` - before the schema is built.
|
|
43
|
+
*/
|
|
44
|
+
function declarePrivileges(where, pairs) {
|
|
45
|
+
for (const [category, privilege] of pairs) {
|
|
46
|
+
if (!category || !privilege)
|
|
47
|
+
throw new Error(`declarePrivileges on ${where} names an empty category or axis`);
|
|
48
|
+
checkAxis(where, privilege, 'declarePrivileges');
|
|
49
|
+
process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
18
52
|
/*
|
|
19
53
|
* ── What each field asks for, by `Type.field` ─────────────────────────────────
|
|
20
54
|
* `PRIVILEGES` above is the list of pairs this installation declares; it cannot answer "what
|
|
@@ -151,20 +185,8 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
|
|
|
151
185
|
`Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +
|
|
152
186
|
`ownership gate. A field open to everyone carries no directive at all.`);
|
|
153
187
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
* Without this, `privilege: "mutaiton"` registered a pair nobody would ever be granted
|
|
158
|
-
* — a gate shut forever, with no error to read. The list is not the framework's: see
|
|
159
|
-
* `privilege-axis.ts` for why, and for the fact that it must be declared at module
|
|
160
|
-
* import time rather than on a bootstrap hook.
|
|
161
|
-
*/
|
|
162
|
-
if (privilege && !(0, privilege_axis_js_1.isDeclaredAxis)(privilege)) {
|
|
163
|
-
throw new Error(`@privilege on ${where} uses axis "${privilege}", which this installation does not declare. ` +
|
|
164
|
-
`Declared axes: ${[...(0, privilege_axis_js_1.declaredAxes)()].sort().join(', ')}. ` +
|
|
165
|
-
`Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +
|
|
166
|
-
`server/index.ts — a bootstrap hook runs after the schema is built and is too late.`);
|
|
167
|
-
}
|
|
188
|
+
if (privilege)
|
|
189
|
+
checkAxis(where, privilege, '@privilege');
|
|
168
190
|
if (category && privilege) {
|
|
169
191
|
process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
|
|
170
192
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"privilege-directive.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-directive.ts"],"names":[],"mappings":"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","sourcesContent":["import { defaultFieldResolver, GraphQLError, GraphQLSchema } from 'graphql'\nimport gql from 'graphql-tag'\n\nimport { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'\nimport { logger } from '@things-factory/env'\n\nimport { judgePermission } from '../../utils/check-permission.js'\nimport { noteAuditAction } from '../audit/audit-recorder.js'\nimport { errorCodeOf, returnedCountOf, targetIdsOf } from '../audit/audit-target.js'\nimport { confirmationRefusal, isRecentlyConfirmed } from '../identity-confirmation/identity-confirmation.js'\nimport { declaredAxes, isDeclaredAxis } from './privilege-axis.js'\nimport { privilegeRejection } from './privilege-rejection.js'\nimport {\n coverageOf,\n DeclaringModule,\n enforcementMode,\n EnforcementMode,\n judgeUndeclared,\n modulesFromLoader\n} from './open-field-rule.js'\n\nprocess['PRIVILEGES'] = {}\n\n/*\n * ── What each field asks for, by `Type.field` ─────────────────────────────────\n * `PRIVILEGES` above is the list of pairs this installation declares; it cannot answer \"what\n * does `Query.jobOrders` ask for\". Something that reaches a door by another entrance has to ask\n * exactly what the door asks — an AI tool is one (operato-twin ADR-0071 decision 2, ADR-0072\n * decision 5). Without this map each caller copied the pair by hand, and a copy drifts the day\n * the door's declaration changes.\n *\n * Three answers, and they are not the same:\n * - not in the map the schema has no such root field. A caller that named it has a typo,\n * and reading that as \"open\" is how a gate opens without an error.\n * - `null` the field exists and declares nothing. It is open.\n * - a gate what `checkPermission` is asked at that field.\n *\n * Every root field is recorded (Query / Mutation / Subscription) so the first two can be told\n * apart; a field of any other type is recorded only when it carries the directive.\n * Kept on `process` for the same reason `PRIVILEGES` is.\n */\nprocess['PRIVILEGE_OF_FIELD'] = {}\n\n/** What `checkPermission` is asked at a field — the same four things the directive passes it. */\nexport interface FieldGate {\n category?: string\n privilege?: string\n owner: boolean\n super: boolean\n}\n\n/**\n * What the field `Type.field` asks for. Valid once the schema has been built.\n *\n * `known: false` means the schema has no such root field — treat it as a mistake, never as open.\n */\nexport function privilegeOfField(typeName: string, fieldName: string): { known: false } | { known: true; gate: FieldGate | null } {\n const map = process['PRIVILEGE_OF_FIELD'] as Record<string, FieldGate | null>\n const where = `${typeName}.${fieldName}`\n return where in map ? { known: true, gate: map[where] } : { known: false }\n}\n\nconst DIRECTIVE = 'privilege'\n\n/**\n * ⚠ **다음에 목록 인자를 붙이는 사람에게.**\n *\n * 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.\n * **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼\n * 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.\n *\n * 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서\n * 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**\n * 요구해야 한다.\n *\n * 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,\n * 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고\n * 말하게** 한다.\n */\nexport const privilegeDirectiveTypeDefs = gql`\n directive @privilege(\n category: String\n privilege: String\n domainOwnerGranted: Boolean\n superUserGranted: Boolean\n ) on FIELD_DEFINITION\n`\n/**\n * The server calls this with the merged schema only. `modules` and `mode` are for tests, which\n * have no module walk of their own (`orderedModuleNames` is empty under jest) — see `open-fields.ts`.\n */\nexport const privilegeDirectiveResolver = (\n schema: GraphQLSchema,\n options: { modules?: DeclaringModule[]; mode?: EnforcementMode } = {}\n) => {\n const coverage = coverageOf(options.modules ?? modulesFromLoader())\n const mode = options.mode ?? enforcementMode()\n const unlisted: string[] = []\n\n const mapped = mapSchema(schema, {\n [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {\n const privilegeDirective = getDirective(schema, fieldConfig, DIRECTIVE)?.[0]\n /* A door that also asks for a recent confirmation that it is still the person (ADR-0168). */\n const asksConfirmation = !!getDirective(schema, fieldConfig, 'recentConfirmation')?.[0]\n\n if (asksConfirmation && !privilegeDirective) {\n throw new Error(\n `@recentConfirmation on ${typeName}.${fieldName} has no @privilege beside it. The confirmation is asked by ` +\n `the privilege gate, after the privilege - a door that asks only for it would ask nobody.`\n )\n }\n\n /* A root field with no directive is recorded as open, so \"open\" and \"no such field\" differ. */\n if (!privilegeDirective && isRootType(schema, typeName)) {\n const where = `${typeName}.${fieldName}`\n\n process['PRIVILEGE_OF_FIELD'][where] = null\n\n /*\n * ── The field that arrived without saying anything ───────────────────\n * Its module opted in (declared `openFields`) and names it in neither list. That is how\n * `deleteAppBinding` went in, and the whole point of §8 step 1 is that it cannot happen\n * quietly any more. A field of a module that has not opted in is left exactly as before.\n */\n if (judgeUndeclared(where, coverage) === 'unlisted') {\n if (mode === 'boot') {\n unlisted.push(where)\n } else {\n refuseUnlisted(fieldConfig, where)\n }\n }\n }\n\n if (privilegeDirective) {\n const { resolve = defaultFieldResolver, args } = fieldConfig\n\n if (!args) {\n throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`)\n }\n\n const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective\n const where = `${typeName}.${fieldName}`\n\n /*\n * The audit's name for this door (ADR-0141 §3.2), and what the field says it acts on (§3.3).\n * A door that asks ownership only is named by the ownership it asks.\n */\n const door =\n category && privilege\n ? `${category}:${privilege}`\n : [domainOwnerGranted && 'owner', superUserGranted && 'super'].filter(Boolean).join('+')\n const target = getDirective(schema, fieldConfig, 'auditTarget')?.[0] as { kind: string; arg: string } | undefined\n\n /*\n * A declaration has to name something.\n *\n * `category` and `privilege` are one thing, not two: together they name a row in the\n * privilege table, and either alone names no row, so `User.hasPrivilege` is never\n * reached and the field falls back on ownership flags it did not set.\n */\n if (!category !== !privilege) {\n throw new Error(\n `@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +\n `The pair names the privilege to look for; one half names none.`\n )\n }\n\n /*\n * `@privilege()` with nothing in it reads like a gate and guards nothing. Until\n * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered\n * false from a safe address and refused everyone, true from an unsafe one and let\n * everyone through. Both are false now, so the shape is merely useless rather than\n * dangerous — and a field that is open should say so by carrying no directive, not by\n * carrying an empty one.\n *\n * It is also the shape someone reaches for wanting \"being signed in is enough\". That\n * is a real thing to want and this directive does not express it; the answer settled\n * on 2026-09-15 is a category for the published side of a module, `<module>-published`.\n *\n * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled\n * for a resolver that declares nothing. A decorator that declares nothing is that\n * same resolver wearing one.\n */\n if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {\n throw new Error(\n `@privilege on ${where} declares nothing. ` +\n `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +\n `ownership gate. A field open to everyone carries no directive at all.`\n )\n }\n\n /*\n * The axis has to be one this installation declared.\n *\n * Without this, `privilege: \"mutaiton\"` registered a pair nobody would ever be granted\n * — a gate shut forever, with no error to read. The list is not the framework's: see\n * `privilege-axis.ts` for why, and for the fact that it must be declared at module\n * import time rather than on a bootstrap hook.\n */\n if (privilege && !isDeclaredAxis(privilege)) {\n throw new Error(\n `@privilege on ${where} uses axis \"${privilege}\", which this installation does not declare. ` +\n `Declared axes: ${[...declaredAxes()].sort().join(', ')}. ` +\n `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +\n `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`\n )\n }\n\n if (category && privilege) {\n process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege]\n }\n\n /* After every refusal above, so a declaration that names nothing never lands here. */\n process['PRIVILEGE_OF_FIELD'][where] = {\n category: category || undefined,\n privilege: privilege || undefined,\n owner: !!domainOwnerGranted,\n super: !!superUserGranted\n } satisfies FieldGate\n\n // 필드의 기존 description 가져오기\n const existingDescription = fieldConfig.description || ''\n\n // 권한 정보를 포함한 새로운 description 생성\n const privilegeDescription =\n `\\n\\n🔒 Requires privilege: ${category}:${privilege}` +\n (domainOwnerGranted ? ', Domain ownership' : '') +\n (superUserGranted ? ', System ownership' : '') +\n (asksConfirmation ? ', a recent confirmation that it is the person' : '')\n\n // 기존 description과 결합\n fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim()\n\n fieldConfig.resolve = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n {\n category,\n privilege,\n owner: domainOwnerGranted,\n super: superUserGranted\n },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n const targetKind = target?.kind\n const targetIds = target ? targetIdsOf(args, target.arg) : undefined\n const targetRole = target ? ('requested' as const) : undefined\n\n if (judgement.permitted === true) {\n /*\n * The privilege is the person's; the confirmation says the person is the one at the screen (ADR-0168).\n * Passing does not lengthen it - it ends ten minutes after it was given.\n */\n if (asksConfirmation && !isRecentlyConfirmed(context, user?.id)) {\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: 'identity-confirmation', targetKind, targetIds, targetRole })\n throw confirmationRefusal(context)\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'granted', basis: judgement.by, targetKind, targetIds, targetRole })\n\n /* Passing the door is not doing the work - what the work came to is its own row (§3.2). */\n try {\n const result = await resolve.call(this, source, args, context, info)\n noteAuditAction({ kind: 'door.result', door, outcome: 'ok', returned: returnedCountOf(result), targetKind, targetIds, targetRole })\n return result\n } catch (error) {\n noteAuditAction({ kind: 'door.result', door, outcome: 'error', code: errorCodeOf(error), targetKind, targetIds, targetRole })\n throw error\n }\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: judgement.because, targetKind, targetIds, targetRole })\n\n /*\n * 안 열렸으면 **거기서 끝이다.**\n *\n * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지\n * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.\n *\n * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.\n */\n throw privilegeRejection(context, category, privilege, judgement.because, info?.operation?.operation)\n }\n\n /*\n * ── A subscription is opened by `subscribe`, not `resolve` ────────────\n * Wrapping only `resolve` left a declared subscription connectable by anyone: the stream\n * opened and every event then failed. It leaked nothing, and it refused nothing either.\n * The same question is asked where the stream starts.\n */\n if (fieldConfig.subscribe) {\n const subscribe = fieldConfig.subscribe\n\n fieldConfig.subscribe = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n { category, privilege, owner: domainOwnerGranted, super: superUserGranted },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n if (judgement.permitted === false) {\n throw privilegeRejection(context, category, privilege, judgement.because, 'subscription')\n }\n\n return await subscribe.call(this, source, args, context, info)\n }\n }\n\n return fieldConfig\n }\n }\n })\n\n /*\n * Every unlisted field at once, rather than the first one. The person who meets this has usually\n * added several resolvers together, and fixing them one boot at a time is how a list gets\n * abandoned half done.\n */\n if (unlisted.length) {\n throw new Error(\n `${unlisted.length} field(s) declare no @privilege and are not listed as open by their module:\\n` +\n unlisted.map(where => ` ${where}`).join('\\n') +\n `\\n\\nFor each, either give it @privilege(category: …, privilege: …), or name it in the module's ` +\n `schema.openFields — under byDesign with the reason it is open, or under notYetDeclared if it ` +\n `is waiting to be closed. See docs/design/auth-rebuild.md §8.`\n )\n }\n\n return mapped\n}\n\n/**\n * Production's answer to an unlisted field: that field refuses, and says so in the log once.\n *\n * The server stays up. What cannot happen is the field answering — it arrived without saying who\n * may call it, and the answer to that is nobody until somebody says.\n */\nconst reported = new Set<string>()\n\nfunction refuseUnlisted(fieldConfig: any, where: string) {\n const refuse = () => {\n if (!reported.has(where)) {\n reported.add(where)\n logger.error(\n `${where} declares no @privilege and is not listed in its module's schema.openFields — refusing it.`\n )\n }\n\n return new GraphQLError(`${where} is not available: it declares no privilege.`, {\n extensions: { code: 'FORBIDDEN', refusal: { code: 'field-declares-no-privilege', params: { field: where } } }\n })\n }\n\n fieldConfig.resolve = async () => {\n throw refuse()\n }\n\n if (fieldConfig.subscribe) {\n fieldConfig.subscribe = async () => {\n throw refuse()\n }\n }\n}\n\nfunction isRootType(schema: GraphQLSchema, typeName: string): boolean {\n return [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].some(\n root => root?.name === typeName\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"privilege-directive.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-directive.ts"],"names":[],"mappings":";;;AAmDA,8CAMC;AAmCD,4CAIC;;AAhGD,qCAA2E;AAC3E,sEAA6B;AAE7B,gDAA0E;AAC1E,6CAA4C;AAE5C,yEAAiE;AACjE,kEAA4D;AAC5D,8DAAoF;AACpF,gGAA4G;AAC5G,2DAAkE;AAClE,qEAA6D;AAC7D,6DAO6B;AAE7B,OAAO,CAAC,YAAY,CAAC,GAAG,EAAE,CAAA;AAE1B,0GAA0G;AAC1G,SAAS,SAAS,CAAC,KAAa,EAAE,SAAiB,EAAE,GAAW;IAC9D;;;;;;;OAOG;IACH,IAAI,CAAC,IAAA,kCAAc,EAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,GAAG,GAAG,OAAO,KAAK,eAAe,SAAS,+CAA+C;YACvF,kBAAkB,CAAC,GAAG,IAAA,gCAAY,GAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC3D,2CAA2C,SAAS,uCAAuC;YAC3F,oFAAoF,CACvF,CAAA;IACH,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,iBAAiB,CAAC,KAAa,EAAE,KAA8C;IAC7F,KAAK,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,KAAK,EAAE,CAAC;QAC1C,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,KAAK,kCAAkC,CAAC,CAAA;QAC7G,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,mBAAmB,CAAC,CAAA;QAChD,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IAC3E,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAA;AAUlC;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,SAAiB;IAClE,MAAM,GAAG,GAAG,OAAO,CAAC,oBAAoB,CAAqC,CAAA;IAC7E,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;IACxC,OAAO,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AAC5E,CAAC;AAED,MAAM,SAAS,GAAG,WAAW,CAAA;AAE7B;;;;;;;;;;;;;;GAcG;AACU,QAAA,0BAA0B,GAAG,IAAA,qBAAG,EAAA;;;;;;;CAO5C,CAAA;AACD;;;GAGG;AACI,MAAM,0BAA0B,GAAG,CACxC,MAAqB,EACrB,UAAmE,EAAE,EACrE,EAAE;IACF,MAAM,QAAQ,GAAG,IAAA,+BAAU,EAAC,OAAO,CAAC,OAAO,IAAI,IAAA,sCAAiB,GAAE,CAAC,CAAA;IACnE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAA,oCAAe,GAAE,CAAA;IAC9C,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,MAAM,MAAM,GAAG,IAAA,iBAAS,EAAC,MAAM,EAAE;QAC/B,CAAC,kBAAU,CAAC,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;YACtE,MAAM,kBAAkB,GAAG,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAC5E,6FAA6F;YAC7F,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAEvF,IAAI,gBAAgB,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAC5C,MAAM,IAAI,KAAK,CACb,0BAA0B,QAAQ,IAAI,SAAS,6DAA6D;oBAC1G,0FAA0F,CAC7F,CAAA;YACH,CAAC;YAED,+FAA+F;YAC/F,IAAI,CAAC,kBAAkB,IAAI,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;gBACxD,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;gBAExC,OAAO,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAA;gBAE3C;;;;;mBAKG;gBACH,IAAI,IAAA,oCAAe,EAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,UAAU,EAAE,CAAC;oBACpD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;wBACpB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;oBACtB,CAAC;yBAAM,CAAC;wBACN,cAAc,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;oBACpC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,IAAI,kBAAkB,EAAE,CAAC;gBACvB,MAAM,EAAE,OAAO,GAAG,8BAAoB,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;gBAE5D,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,MAAM,IAAI,KAAK,CAAC,8EAA8E,SAAS,GAAG,CAAC,CAAA;gBAC7G,CAAC;gBAED,MAAM,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,kBAAkB,CAAA;gBACxF,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;gBAExC;;;mBAGG;gBACH,MAAM,IAAI,GACR,QAAQ,IAAI,SAAS;oBACnB,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE;oBAC5B,CAAC,CAAC,CAAC,kBAAkB,IAAI,OAAO,EAAE,gBAAgB,IAAI,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC5F,MAAM,MAAM,GAAG,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,CAA8C,CAAA;gBAEjH;;;;;;mBAMG;gBACH,IAAI,CAAC,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;oBAC7B,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,8BAA8B,IAAI;wBAC5G,gEAAgE,CACnE,CAAA;gBACH,CAAC;gBAED;;;;;;;;;;;;;;;mBAeG;gBACH,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,IAAI,CAAC,kBAAkB,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACxE,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,qBAAqB;wBACzC,mGAAmG;wBACnG,uEAAuE,CAC1E,CAAA;gBACH,CAAC;gBAED,IAAI,SAAS;oBAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,CAAA;gBAExD,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;oBAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;gBAC3E,CAAC;gBAED,sFAAsF;gBACtF,OAAO,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG;oBACrC,QAAQ,EAAE,QAAQ,IAAI,SAAS;oBAC/B,SAAS,EAAE,SAAS,IAAI,SAAS;oBACjC,KAAK,EAAE,CAAC,CAAC,kBAAkB;oBAC3B,KAAK,EAAE,CAAC,CAAC,gBAAgB;iBACN,CAAA;gBAErB,0BAA0B;gBAC1B,MAAM,mBAAmB,GAAG,WAAW,CAAC,WAAW,IAAI,EAAE,CAAA;gBAEzD,gCAAgC;gBAChC,MAAM,oBAAoB,GACxB,8BAA8B,QAAQ,IAAI,SAAS,EAAE;oBACrD,CAAC,kBAAkB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;oBAChD,CAAC,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC9C,CAAC,gBAAgB,CAAC,CAAC,CAAC,+CAA+C,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAE3E,qBAAqB;gBACrB,WAAW,CAAC,WAAW,GAAG,GAAG,mBAAmB,IAAI,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAA;gBAEjF,WAAW,CAAC,OAAO,GAAG,KAAK,WAAW,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;oBAC/D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;oBAEtE,MAAM,SAAS,GAAG,MAAM,IAAA,qCAAe,EACrC;wBACE,QAAQ;wBACR,SAAS;wBACT,KAAK,EAAE,kBAAkB;wBACzB,KAAK,EAAE,gBAAgB;qBACxB,EACD,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;oBAED,MAAM,UAAU,GAAG,MAAM,EAAE,IAAI,CAAA;oBAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,IAAA,6BAAW,EAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;oBACpE,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAE,WAAqB,CAAC,CAAC,CAAC,SAAS,CAAA;oBAE9D,IAAI,SAAS,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;wBACjC;;;2BAGG;wBACH,IAAI,gBAAgB,IAAI,CAAC,IAAA,8CAAmB,EAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;4BAChE,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,uBAAuB,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;4BACpI,MAAM,IAAA,8CAAmB,EAAC,OAAO,CAAC,CAAA;wBACpC,CAAC;wBAED,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;wBAE1H,2FAA2F;wBAC3F,IAAI,CAAC;4BACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;4BACpE,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAA,iCAAe,EAAC,MAAM,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;4BACnI,OAAO,MAAM,CAAA;wBACf,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BACf,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAA,6BAAW,EAAC,KAAK,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;4BAC7H,MAAM,KAAK,CAAA;wBACb,CAAC;oBACH,CAAC;oBAED,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;oBAE9H;;;;;;;uBAOG;oBACH,MAAM,IAAA,2CAAkB,EAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,CAAA;gBACvG,CAAC,CAAA;gBAED;;;;;mBAKG;gBACH,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;oBAC1B,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAA;oBAEvC,WAAW,CAAC,SAAS,GAAG,KAAK,WAAW,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;wBACjE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;wBAEtE,MAAM,SAAS,GAAG,MAAM,IAAA,qCAAe,EACrC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAC3E,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;wBAED,IAAI,SAAS,CAAC,SAAS,KAAK,KAAK,EAAE,CAAC;4BAClC,MAAM,IAAA,2CAAkB,EAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;wBAC3F,CAAC;wBAED,OAAO,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;oBAChE,CAAC,CAAA;gBACH,CAAC;gBAED,OAAO,WAAW,CAAA;YACpB,CAAC;QACH,CAAC;KACF,CAAC,CAAA;IAEF;;;;OAIG;IACH,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,CAAC,MAAM,+EAA+E;YAC/F,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAC9C,iGAAiG;YACjG,+FAA+F;YAC/F,8DAA8D,CACjE,CAAA;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAxOY,QAAA,0BAA0B,8BAwOtC;AAED;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAA;AAElC,SAAS,cAAc,CAAC,WAAgB,EAAE,KAAa;IACrD,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACnB,YAAM,CAAC,KAAK,CACV,GAAG,KAAK,4FAA4F,CACrG,CAAA;QACH,CAAC;QAED,OAAO,IAAI,sBAAY,CAAC,GAAG,KAAK,8CAA8C,EAAE;YAC9E,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,6BAA6B,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;SAC9G,CAAC,CAAA;IACJ,CAAC,CAAA;IAED,WAAW,CAAC,OAAO,GAAG,KAAK,IAAI,EAAE;QAC/B,MAAM,MAAM,EAAE,CAAA;IAChB,CAAC,CAAA;IAED,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;QAC1B,WAAW,CAAC,SAAS,GAAG,KAAK,IAAI,EAAE;YACjC,MAAM,MAAM,EAAE,CAAA;QAChB,CAAC,CAAA;IACH,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAqB,EAAE,QAAgB;IACzD,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,MAAM,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC,IAAI,CACzF,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,CAChC,CAAA;AACH,CAAC","sourcesContent":["import { defaultFieldResolver, GraphQLError, GraphQLSchema } from 'graphql'\nimport gql from 'graphql-tag'\n\nimport { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'\nimport { logger } from '@things-factory/env'\n\nimport { judgePermission } from '../../utils/check-permission.js'\nimport { noteAuditAction } from '../audit/audit-recorder.js'\nimport { errorCodeOf, returnedCountOf, targetIdsOf } from '../audit/audit-target.js'\nimport { confirmationRefusal, isRecentlyConfirmed } from '../identity-confirmation/identity-confirmation.js'\nimport { declaredAxes, isDeclaredAxis } from './privilege-axis.js'\nimport { privilegeRejection } from './privilege-rejection.js'\nimport {\n coverageOf,\n DeclaringModule,\n enforcementMode,\n EnforcementMode,\n judgeUndeclared,\n modulesFromLoader\n} from './open-field-rule.js'\n\nprocess['PRIVILEGES'] = {}\n\n/** The axis check every declaration passes, whoever declares it — `@privilege` or `declarePrivileges`. */\nfunction checkAxis(where: string, privilege: string, how: string) {\n /*\n * The axis has to be one this installation declared.\n *\n * Without this, `privilege: \"mutaiton\"` registered a pair nobody would ever be granted\n * — a gate shut forever, with no error to read. The list is not the framework's: see\n * `privilege-axis.ts` for why, and for the fact that it must be declared at module\n * import time rather than on a bootstrap hook.\n */\n if (!isDeclaredAxis(privilege)) {\n throw new Error(\n `${how} on ${where} uses axis \"${privilege}\", which this installation does not declare. ` +\n `Declared axes: ${[...declaredAxes()].sort().join(', ')}. ` +\n `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +\n `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`\n )\n }\n}\n\n/**\n * Declares privileges a module asks in code (`checkPermission`) and no field asks with `@privilege` — a rule an issued\n * document kind or a worklist step holds. The privilege sync creates rows only for what is declared, and a role task\n * naming an undeclared pair is refused when a template is applied (ADR-0176, 2026-10-30: `employee-certificate` was\n * asked in code only, and `office.finance` could not be applied).\n *\n * Call it at the top level of the file that asks them, as `declareAxes` - before the schema is built.\n */\nexport function declarePrivileges(where: string, pairs: [category: string, privilege: string][]): void {\n for (const [category, privilege] of pairs) {\n if (!category || !privilege) throw new Error(`declarePrivileges on ${where} names an empty category or axis`)\n checkAxis(where, privilege, 'declarePrivileges')\n process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege]\n }\n}\n\n/*\n * ── What each field asks for, by `Type.field` ─────────────────────────────────\n * `PRIVILEGES` above is the list of pairs this installation declares; it cannot answer \"what\n * does `Query.jobOrders` ask for\". Something that reaches a door by another entrance has to ask\n * exactly what the door asks — an AI tool is one (operato-twin ADR-0071 decision 2, ADR-0072\n * decision 5). Without this map each caller copied the pair by hand, and a copy drifts the day\n * the door's declaration changes.\n *\n * Three answers, and they are not the same:\n * - not in the map the schema has no such root field. A caller that named it has a typo,\n * and reading that as \"open\" is how a gate opens without an error.\n * - `null` the field exists and declares nothing. It is open.\n * - a gate what `checkPermission` is asked at that field.\n *\n * Every root field is recorded (Query / Mutation / Subscription) so the first two can be told\n * apart; a field of any other type is recorded only when it carries the directive.\n * Kept on `process` for the same reason `PRIVILEGES` is.\n */\nprocess['PRIVILEGE_OF_FIELD'] = {}\n\n/** What `checkPermission` is asked at a field — the same four things the directive passes it. */\nexport interface FieldGate {\n category?: string\n privilege?: string\n owner: boolean\n super: boolean\n}\n\n/**\n * What the field `Type.field` asks for. Valid once the schema has been built.\n *\n * `known: false` means the schema has no such root field — treat it as a mistake, never as open.\n */\nexport function privilegeOfField(typeName: string, fieldName: string): { known: false } | { known: true; gate: FieldGate | null } {\n const map = process['PRIVILEGE_OF_FIELD'] as Record<string, FieldGate | null>\n const where = `${typeName}.${fieldName}`\n return where in map ? { known: true, gate: map[where] } : { known: false }\n}\n\nconst DIRECTIVE = 'privilege'\n\n/**\n * ⚠ **다음에 목록 인자를 붙이는 사람에게.**\n *\n * 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.\n * **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼\n * 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.\n *\n * 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서\n * 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**\n * 요구해야 한다.\n *\n * 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,\n * 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고\n * 말하게** 한다.\n */\nexport const privilegeDirectiveTypeDefs = gql`\n directive @privilege(\n category: String\n privilege: String\n domainOwnerGranted: Boolean\n superUserGranted: Boolean\n ) on FIELD_DEFINITION\n`\n/**\n * The server calls this with the merged schema only. `modules` and `mode` are for tests, which\n * have no module walk of their own (`orderedModuleNames` is empty under jest) — see `open-fields.ts`.\n */\nexport const privilegeDirectiveResolver = (\n schema: GraphQLSchema,\n options: { modules?: DeclaringModule[]; mode?: EnforcementMode } = {}\n) => {\n const coverage = coverageOf(options.modules ?? modulesFromLoader())\n const mode = options.mode ?? enforcementMode()\n const unlisted: string[] = []\n\n const mapped = mapSchema(schema, {\n [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {\n const privilegeDirective = getDirective(schema, fieldConfig, DIRECTIVE)?.[0]\n /* A door that also asks for a recent confirmation that it is still the person (ADR-0168). */\n const asksConfirmation = !!getDirective(schema, fieldConfig, 'recentConfirmation')?.[0]\n\n if (asksConfirmation && !privilegeDirective) {\n throw new Error(\n `@recentConfirmation on ${typeName}.${fieldName} has no @privilege beside it. The confirmation is asked by ` +\n `the privilege gate, after the privilege - a door that asks only for it would ask nobody.`\n )\n }\n\n /* A root field with no directive is recorded as open, so \"open\" and \"no such field\" differ. */\n if (!privilegeDirective && isRootType(schema, typeName)) {\n const where = `${typeName}.${fieldName}`\n\n process['PRIVILEGE_OF_FIELD'][where] = null\n\n /*\n * ── The field that arrived without saying anything ───────────────────\n * Its module opted in (declared `openFields`) and names it in neither list. That is how\n * `deleteAppBinding` went in, and the whole point of §8 step 1 is that it cannot happen\n * quietly any more. A field of a module that has not opted in is left exactly as before.\n */\n if (judgeUndeclared(where, coverage) === 'unlisted') {\n if (mode === 'boot') {\n unlisted.push(where)\n } else {\n refuseUnlisted(fieldConfig, where)\n }\n }\n }\n\n if (privilegeDirective) {\n const { resolve = defaultFieldResolver, args } = fieldConfig\n\n if (!args) {\n throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`)\n }\n\n const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective\n const where = `${typeName}.${fieldName}`\n\n /*\n * The audit's name for this door (ADR-0141 §3.2), and what the field says it acts on (§3.3).\n * A door that asks ownership only is named by the ownership it asks.\n */\n const door =\n category && privilege\n ? `${category}:${privilege}`\n : [domainOwnerGranted && 'owner', superUserGranted && 'super'].filter(Boolean).join('+')\n const target = getDirective(schema, fieldConfig, 'auditTarget')?.[0] as { kind: string; arg: string } | undefined\n\n /*\n * A declaration has to name something.\n *\n * `category` and `privilege` are one thing, not two: together they name a row in the\n * privilege table, and either alone names no row, so `User.hasPrivilege` is never\n * reached and the field falls back on ownership flags it did not set.\n */\n if (!category !== !privilege) {\n throw new Error(\n `@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +\n `The pair names the privilege to look for; one half names none.`\n )\n }\n\n /*\n * `@privilege()` with nothing in it reads like a gate and guards nothing. Until\n * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered\n * false from a safe address and refused everyone, true from an unsafe one and let\n * everyone through. Both are false now, so the shape is merely useless rather than\n * dangerous — and a field that is open should say so by carrying no directive, not by\n * carrying an empty one.\n *\n * It is also the shape someone reaches for wanting \"being signed in is enough\". That\n * is a real thing to want and this directive does not express it; the answer settled\n * on 2026-09-15 is a category for the published side of a module, `<module>-published`.\n *\n * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled\n * for a resolver that declares nothing. A decorator that declares nothing is that\n * same resolver wearing one.\n */\n if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {\n throw new Error(\n `@privilege on ${where} declares nothing. ` +\n `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +\n `ownership gate. A field open to everyone carries no directive at all.`\n )\n }\n\n if (privilege) checkAxis(where, privilege, '@privilege')\n\n if (category && privilege) {\n process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege]\n }\n\n /* After every refusal above, so a declaration that names nothing never lands here. */\n process['PRIVILEGE_OF_FIELD'][where] = {\n category: category || undefined,\n privilege: privilege || undefined,\n owner: !!domainOwnerGranted,\n super: !!superUserGranted\n } satisfies FieldGate\n\n // 필드의 기존 description 가져오기\n const existingDescription = fieldConfig.description || ''\n\n // 권한 정보를 포함한 새로운 description 생성\n const privilegeDescription =\n `\\n\\n🔒 Requires privilege: ${category}:${privilege}` +\n (domainOwnerGranted ? ', Domain ownership' : '') +\n (superUserGranted ? ', System ownership' : '') +\n (asksConfirmation ? ', a recent confirmation that it is the person' : '')\n\n // 기존 description과 결합\n fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim()\n\n fieldConfig.resolve = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n {\n category,\n privilege,\n owner: domainOwnerGranted,\n super: superUserGranted\n },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n const targetKind = target?.kind\n const targetIds = target ? targetIdsOf(args, target.arg) : undefined\n const targetRole = target ? ('requested' as const) : undefined\n\n if (judgement.permitted === true) {\n /*\n * The privilege is the person's; the confirmation says the person is the one at the screen (ADR-0168).\n * Passing does not lengthen it - it ends ten minutes after it was given.\n */\n if (asksConfirmation && !isRecentlyConfirmed(context, user?.id)) {\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: 'identity-confirmation', targetKind, targetIds, targetRole })\n throw confirmationRefusal(context)\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'granted', basis: judgement.by, targetKind, targetIds, targetRole })\n\n /* Passing the door is not doing the work - what the work came to is its own row (§3.2). */\n try {\n const result = await resolve.call(this, source, args, context, info)\n noteAuditAction({ kind: 'door.result', door, outcome: 'ok', returned: returnedCountOf(result), targetKind, targetIds, targetRole })\n return result\n } catch (error) {\n noteAuditAction({ kind: 'door.result', door, outcome: 'error', code: errorCodeOf(error), targetKind, targetIds, targetRole })\n throw error\n }\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: judgement.because, targetKind, targetIds, targetRole })\n\n /*\n * 안 열렸으면 **거기서 끝이다.**\n *\n * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지\n * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.\n *\n * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.\n */\n throw privilegeRejection(context, category, privilege, judgement.because, info?.operation?.operation)\n }\n\n /*\n * ── A subscription is opened by `subscribe`, not `resolve` ────────────\n * Wrapping only `resolve` left a declared subscription connectable by anyone: the stream\n * opened and every event then failed. It leaked nothing, and it refused nothing either.\n * The same question is asked where the stream starts.\n */\n if (fieldConfig.subscribe) {\n const subscribe = fieldConfig.subscribe\n\n fieldConfig.subscribe = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n { category, privilege, owner: domainOwnerGranted, super: superUserGranted },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n if (judgement.permitted === false) {\n throw privilegeRejection(context, category, privilege, judgement.because, 'subscription')\n }\n\n return await subscribe.call(this, source, args, context, info)\n }\n }\n\n return fieldConfig\n }\n }\n })\n\n /*\n * Every unlisted field at once, rather than the first one. The person who meets this has usually\n * added several resolvers together, and fixing them one boot at a time is how a list gets\n * abandoned half done.\n */\n if (unlisted.length) {\n throw new Error(\n `${unlisted.length} field(s) declare no @privilege and are not listed as open by their module:\\n` +\n unlisted.map(where => ` ${where}`).join('\\n') +\n `\\n\\nFor each, either give it @privilege(category: …, privilege: …), or name it in the module's ` +\n `schema.openFields — under byDesign with the reason it is open, or under notYetDeclared if it ` +\n `is waiting to be closed. See docs/design/auth-rebuild.md §8.`\n )\n }\n\n return mapped\n}\n\n/**\n * Production's answer to an unlisted field: that field refuses, and says so in the log once.\n *\n * The server stays up. What cannot happen is the field answering — it arrived without saying who\n * may call it, and the answer to that is nobody until somebody says.\n */\nconst reported = new Set<string>()\n\nfunction refuseUnlisted(fieldConfig: any, where: string) {\n const refuse = () => {\n if (!reported.has(where)) {\n reported.add(where)\n logger.error(\n `${where} declares no @privilege and is not listed in its module's schema.openFields — refusing it.`\n )\n }\n\n return new GraphQLError(`${where} is not available: it declares no privilege.`, {\n extensions: { code: 'FORBIDDEN', refusal: { code: 'field-declares-no-privilege', params: { field: where } } }\n })\n }\n\n fieldConfig.resolve = async () => {\n throw refuse()\n }\n\n if (fieldConfig.subscribe) {\n fieldConfig.subscribe = async () => {\n throw refuse()\n }\n }\n}\n\nfunction isRootType(schema: GraphQLSchema, typeName: string): boolean {\n return [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].some(\n root => root?.name === typeName\n )\n}\n"]}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { WebAuthCredential } from './web-auth-credential.js';
|
|
2
|
+
/**
|
|
3
|
+
* The caller's own passkeys (ADR-0156 decision 6). Another person's id is answered as one that does
|
|
4
|
+
* not exist - the answer does not tell whether it is somebody else's.
|
|
5
|
+
*/
|
|
6
|
+
/** A passkey name's bound - the screen's maxlength (auth-ui my-passkeys). */
|
|
7
|
+
export declare const PASSKEY_NAME_MAX = 64;
|
|
2
8
|
export declare class WebAuthCredentialMutation {
|
|
3
9
|
/** Open by design: renames only the caller's own passkey. */
|
|
4
10
|
updateMyWebAuthCredential(id: string, name: string, context: ResolverContext): Promise<WebAuthCredential>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WebAuthCredentialMutation = void 0;
|
|
3
|
+
exports.WebAuthCredentialMutation = exports.PASSKEY_NAME_MAX = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const type_graphql_1 = require("type-graphql");
|
|
6
6
|
const shell_1 = require("@things-factory/shell");
|
|
@@ -10,6 +10,8 @@ const web_auth_credential_js_1 = require("./web-auth-credential.js");
|
|
|
10
10
|
* The caller's own passkeys (ADR-0156 decision 6). Another person's id is answered as one that does
|
|
11
11
|
* not exist - the answer does not tell whether it is somebody else's.
|
|
12
12
|
*/
|
|
13
|
+
/** A passkey name's bound - the screen's maxlength (auth-ui my-passkeys). */
|
|
14
|
+
exports.PASSKEY_NAME_MAX = 64;
|
|
13
15
|
async function ownPasskey(id, context) {
|
|
14
16
|
const { user, tx } = context.state;
|
|
15
17
|
const credential = await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential, tx).findOne({ where: { id, user: { id: user.id } } });
|
|
@@ -23,9 +25,14 @@ let WebAuthCredentialMutation = class WebAuthCredentialMutation {
|
|
|
23
25
|
async updateMyWebAuthCredential(id, name, context) {
|
|
24
26
|
const { user, tx } = context.state;
|
|
25
27
|
const credential = await ownPasskey(id, context);
|
|
28
|
+
/* A longer name was cut at 64 without a word (inline help audit 2026-10-30) - it is refused with the bound. */
|
|
29
|
+
const given = name.trim();
|
|
30
|
+
if (given.length > exports.PASSKEY_NAME_MAX) {
|
|
31
|
+
throw new shell_1.Refusal('passkey-name-too-long', { max: exports.PASSKEY_NAME_MAX, length: given.length }, `A passkey name takes up to ${exports.PASSKEY_NAME_MAX} characters.`);
|
|
32
|
+
}
|
|
26
33
|
return await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential, tx).save({
|
|
27
34
|
...credential,
|
|
28
|
-
name:
|
|
35
|
+
name: given || null,
|
|
29
36
|
updater: user
|
|
30
37
|
});
|
|
31
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-auth-credential-mutation.js","sourceRoot":"","sources":["../../../server/service/web-auth-credential/web-auth-credential-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAAqF;AAErF,iDAA6E;AAE7E,kEAA8D;AAC9D,qEAA4D;AAE5D;;;GAGG;AACH,KAAK,UAAU,UAAU,CAAC,EAAU,EAAE,OAAwB;IAC5D,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElC,MAAM,UAAU,GAAG,MAAM,IAAA,qBAAa,EAAC,0CAAiB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;IAC/G,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,EAAE,EAAE,EAAE,4CAA4C,CAAC,CAAA;IAC9F,CAAC;IAED,OAAO,UAAU,CAAA;AACnB,CAAC;AAIM,IAAM,yBAAyB,GAA/B,MAAM,yBAAyB;IACpC,6DAA6D;IAGvD,AAAN,KAAK,CAAC,yBAAyB,CAClB,EAAU,EACR,IAAY,EAClB,OAAwB;QAE/B,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAClC,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"web-auth-credential-mutation.js","sourceRoot":"","sources":["../../../server/service/web-auth-credential/web-auth-credential-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { getRepository, Refusal, SpeakRefusals } from '@things-factory/shell'\n\nimport { recordAuditAction } from '../audit/audit-recorder.js'\nimport { WebAuthCredential } from './web-auth-credential.js'\n\n/**\n * The caller's own passkeys (ADR-0156 decision 6). Another person's id is answered as one that does\n * not exist - the answer does not tell whether it is somebody else's.\n */\n/** A passkey name's bound - the screen's maxlength (auth-ui my-passkeys). */\nexport const PASSKEY_NAME_MAX = 64\n\nasync function ownPasskey(id: string, context: ResolverContext): Promise<WebAuthCredential> {\n const { user, tx } = context.state\n\n const credential = await getRepository(WebAuthCredential, tx).findOne({ where: { id, user: { id: user.id } } })\n if (!credential) {\n throw new Refusal('passkey-not-found', { id }, 'There is no passkey of yours with this id.')\n }\n\n return credential\n}\n\n@Resolver(WebAuthCredential)\n@UseMiddleware(SpeakRefusals)\nexport class WebAuthCredentialMutation {\n /** Open by design: renames only the caller's own passkey. */\n @Directive('@transaction')\n @Mutation(returns => WebAuthCredential, { description: \"To rename one of the caller's own passkeys.\" })\n async updateMyWebAuthCredential(\n @Arg('id') id: string,\n @Arg('name') name: string,\n @Ctx() context: ResolverContext\n ): Promise<WebAuthCredential> {\n const { user, tx } = context.state\n const credential = await ownPasskey(id, context)\n /* A longer name was cut at 64 without a word (inline help audit 2026-10-30) - it is refused with the bound. */\n const given = name.trim()\n if (given.length > PASSKEY_NAME_MAX) {\n throw new Refusal('passkey-name-too-long', { max: PASSKEY_NAME_MAX, length: given.length }, `A passkey name takes up to ${PASSKEY_NAME_MAX} characters.`)\n }\n\n return await getRepository(WebAuthCredential, tx).save({\n ...credential,\n name: given || null,\n updater: user\n })\n }\n\n /** Open by design: removes only the caller's own passkey. It signs nobody in afterwards. */\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: \"To remove one of the caller's own passkeys. It can no longer sign in.\" })\n async deleteMyWebAuthCredential(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { user, tx } = context.state\n const credential = await ownPasskey(id, context)\n const repository = getRepository(WebAuthCredential, tx)\n\n await repository.update({ id: credential.id }, { updater: user })\n await repository.softDelete({ id: credential.id })\n await recordAuditAction(\n { kind: 'webauthn.remove', targetKind: 'User', targetIds: [user.id], targetRole: 'handled' },\n { manager: tx, actorId: user.id }\n )\n\n return true\n }\n}\n"]}
|