@things-factory/auth-base 10.1.6 → 10.1.16

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist-client/tsconfig.tsbuildinfo +1 -1
  3. package/dist-server/index.js +16 -0
  4. package/dist-server/index.js.map +1 -1
  5. package/dist-server/router/auth-private-process-router.js +20 -1
  6. package/dist-server/router/auth-private-process-router.js.map +1 -1
  7. package/dist-server/service/index.d.ts +3 -0
  8. package/dist-server/service/index.js +16 -11
  9. package/dist-server/service/index.js.map +1 -1
  10. package/dist-server/service/privilege/privilege-axis.d.ts +72 -0
  11. package/dist-server/service/privilege/privilege-axis.js +188 -0
  12. package/dist-server/service/privilege/privilege-axis.js.map +1 -0
  13. package/dist-server/service/privilege/privilege-directive.d.ts +10 -41
  14. package/dist-server/service/privilege/privilege-directive.js +62 -74
  15. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  16. package/dist-server/service/role/role-mutation.js +2 -2
  17. package/dist-server/service/role/role-mutation.js.map +1 -1
  18. package/dist-server/service/role/role-query.d.ts +43 -1
  19. package/dist-server/service/role/role-query.js +76 -33
  20. package/dist-server/service/role/role-query.js.map +1 -1
  21. package/dist-server/service/role-template/index.d.ts +4 -0
  22. package/dist-server/service/role-template/index.js +9 -0
  23. package/dist-server/service/role-template/index.js.map +1 -0
  24. package/dist-server/service/role-template/role-template-mutation.d.ts +39 -0
  25. package/dist-server/service/role-template/role-template-mutation.js +124 -0
  26. package/dist-server/service/role-template/role-template-mutation.js.map +1 -0
  27. package/dist-server/service/role-template/role-template-query.d.ts +11 -0
  28. package/dist-server/service/role-template/role-template-query.js +79 -0
  29. package/dist-server/service/role-template/role-template-query.js.map +1 -0
  30. package/dist-server/service/role-template/role-template-types.d.ts +51 -0
  31. package/dist-server/service/role-template/role-template-types.js +71 -0
  32. package/dist-server/service/role-template/role-template-types.js.map +1 -0
  33. package/dist-server/service/role-template/role-template.d.ts +110 -0
  34. package/dist-server/service/role-template/role-template.js +147 -0
  35. package/dist-server/service/role-template/role-template.js.map +1 -0
  36. package/dist-server/tsconfig.tsbuildinfo +1 -1
  37. package/dist-server/utils/check-permission.js +11 -2
  38. package/dist-server/utils/check-permission.js.map +1 -1
  39. package/package.json +4 -4
  40. package/tests/domain-inheritance-sentinel.test.ts +93 -0
  41. package/tests/permission-gate.test.ts +16 -12
  42. package/tests/privilege-axis.test.ts +157 -0
  43. package/tests/privilege-directive.test.ts +70 -81
  44. package/tests/role-privileges-db.test.ts +98 -0
  45. package/tests/role-template-seed-db.test.ts +179 -0
  46. package/tests/role-template.test.ts +162 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,oDAAuG;AACvG,uDAA8G;AAC9G,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,+CAAsF;AACtF,+CAAsF;AACtF,6DAAqF;AACrF,8DAAsF;AACtF,sDAAwG;AACxG,uDAA4G;AAE5G,yBAAyB;AACzB,yFAA8D;AAC9D,2EAAgD;AAChD,uEAA4C;AAC5C,mEAAwC;AACxC,mEAAwC;AACxC,yDAA8B;AAC9B,yDAA8B;AAC9B,+DAAoC;AACpC,yEAA8C;AAC9C,qEAA0C;AAC1C,uEAA4C;AAC5C,iFAAsD;AACtD,qFAA0D;AAC1D,2EAAgD;AAChD,uFAA4D;AAC5D,uEAA4C;AAC5C,yEAA8C;AAE9C,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,2EAAgD;AAChD,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAErC,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAA0B;IAC7B,GAAG,mBAAoB;IACvB,GAAG,mBAAmB;IACtB,GAAG,mBAAiB;IACpB,GAAG,oBAAiB;IACpB,GAAG,oBAAY;IACf,GAAG,oBAAY;IACf,GAAG,oBAAe;IAClB,GAAG,mBAAmB;IACtB,GAAG,mBAAkB;IACrB,GAAG,oBAAuB;IAC1B,GAAG,oBAAyB;IAC5B,GAAG,mBAAoB;IACvB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;CACvB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;KAC3B;IAED,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAA2B;QAC9B,GAAG,oBAAqB;QACxB,GAAG,oBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAkB;QACrB,GAAG,qBAAa;QAChB,GAAG,qBAAa;QAChB,GAAG,qBAAgB;QACnB,GAAG,oBAAmB;QACtB,GAAG,oBAAkB;QACrB,GAAG,oBAAkB;QACrB,GAAG,oBAAuB;QAC1B,GAAG,oBAAoB;QACvB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;KACxB;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport {\n entities as UsersAuthProvidersEntities,\n resolvers as UsersAuthProvidersResolvers\n} from './users-auth-providers/index.js'\nimport { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider/index.js'\nimport { resolvers as AppbindingResolver } from './app-binding/index.js'\nimport { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance/index.js'\nimport { entities as ApplicationEntities, resolvers as ApplicationResolvers } from './application/index.js'\nimport { resolvers as DomainGeneratorResolver } from './domain-generator/index.js'\nimport { entities as GrantedRoleEntities, resolvers as GrantedRoleResolver } from './granted-role/index.js'\nimport { entities as InvitationEntities, resolvers as InvitationResolver } from './invitation/index.js'\nimport { entities as LoginHistoryEntities, resolvers as LoginHistoryResolver } from './login-history/index.js'\nimport { entities as PartnerEntities, resolvers as PartnerResolvers } from './partner/index.js'\nimport { entities as PasswordHistoryEntities } from './password-history/index.js'\nimport { entities as PrivilegeEntities, resolvers as PrivilegeResolvers } from './privilege/index.js'\nimport { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privilege/privilege-directive.js'\nimport { entities as RoleEntities, resolvers as RoleResolvers } from './role/index.js'\nimport { entities as UserEntities, resolvers as UserResolvers } from './user/index.js'\nimport { entities as VerificationTokenEntities } from './verification-token/index.js'\nimport { entities as WebAuthCredentialEntities } from './web-auth-credential/index.js'\nimport { entities as DomainLinkEntities, resolvers as DomainLinkResolver } from './domain-link/index.js'\nimport { entities as DomainOwnerEntities, resolvers as DomainOwnerResolvers } from './domain-owner/index.js'\n\n/* EXPORT ENTITY TYPES */\nexport * from './users-auth-providers/users-auth-providers.js'\nexport * from './auth-provider/auth-provider.js'\nexport * from './application/application.js'\nexport * from './appliance/appliance.js'\nexport * from './privilege/privilege.js'\nexport * from './role/role.js'\nexport * from './user/user.js'\nexport * from './partner/partner.js'\nexport * from './granted-role/granted-role.js'\nexport * from './invitation/invitation.js'\nexport * from './app-binding/app-binding.js'\nexport * from './password-history/password-history.js'\nexport * from './verification-token/verification-token.js'\nexport * from './login-history/login-history.js'\nexport * from './web-auth-credential/web-auth-credential.js'\nexport * from './domain-link/domain-link.js'\nexport * from './domain-owner/domain-owner.js'\n\n/* EXPORT TYPES */\nexport * from './app-binding/app-binding-types.js'\nexport * from './appliance/appliance-types.js'\nexport * from './application/application-types.js'\nexport * from './domain-generator/domain-generator-types.js'\nexport * from './invitation/invitation-types.js'\nexport * from './partner/partner-types.js'\nexport * from './privilege/privilege-types.js'\nexport * from './role/role-types.js'\nexport * from './user/user-types.js'\nexport * from './domain-link/domain-link-types.js'\n\nexport const entities = [\n /* ENTITIES */\n ...UsersAuthProvidersEntities,\n ...AuthProviderEntities,\n ...ApplicationEntities,\n ...ApplianceEntities,\n ...PrivilegeEntities,\n ...RoleEntities,\n ...UserEntities,\n ...PartnerEntities,\n ...GrantedRoleEntities,\n ...InvitationEntities,\n ...PasswordHistoryEntities,\n ...VerificationTokenEntities,\n ...LoginHistoryEntities,\n ...WebAuthCredentialEntities,\n ...DomainLinkEntities,\n ...DomainOwnerEntities\n]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs\n },\n\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...UsersAuthProvidersResolvers,\n ...AuthProviderResolvers,\n ...ApplicationResolvers,\n ...ApplianceResolvers,\n ...PrivilegeResolvers,\n ...RoleResolvers,\n ...UserResolvers,\n ...PartnerResolvers,\n ...GrantedRoleResolver,\n ...InvitationResolver,\n ...AppbindingResolver,\n ...DomainGeneratorResolver,\n ...LoginHistoryResolver,\n ...DomainLinkResolver,\n ...DomainOwnerResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,oDAAuG;AACvG,uDAA8G;AAC9G,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,+CAAsF;AACtF,wDAA6E;AAC7E,+CAAsF;AACtF,6DAAqF;AACrF,8DAAsF;AACtF,sDAAwG;AACxG,uDAA4G;AAE5G,yBAAyB;AACzB,yFAA8D;AAC9D,2EAAgD;AAChD,uEAA4C;AAC5C,mEAAwC;AACxC,mEAAwC;AACxC,yDAA8B;AAC9B,wEAA6C;AAC7C,2EAAgD;AAChD,iFAAsD;AACtD,yDAA8B;AAC9B,+DAAoC;AACpC,yEAA8C;AAC9C,qEAA0C;AAC1C,uEAA4C;AAC5C,iFAAsD;AACtD,qFAA0D;AAC1D,2EAAgD;AAChD,uFAA4D;AAC5D,uEAA4C;AAC5C,yEAA8C;AAE9C,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,2EAAgD;AAChD,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAErC,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAA0B;IAC7B,GAAG,mBAAoB;IACvB,GAAG,mBAAmB;IACtB,GAAG,mBAAiB;IACpB,GAAG,oBAAiB;IACpB,GAAG,oBAAY;IACf,GAAG,oBAAY;IACf,GAAG,oBAAe;IAClB,GAAG,mBAAmB;IACtB,GAAG,mBAAkB;IACrB,GAAG,oBAAuB;IAC1B,GAAG,oBAAyB;IAC5B,GAAG,mBAAoB;IACvB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;CACvB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;KAC3B;IAED,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAA2B;QAC9B,GAAG,oBAAqB;QACxB,GAAG,oBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAkB;QACrB,GAAG,qBAAa;QAChB,GAAG,qBAAqB;QACxB,GAAG,qBAAa;QAChB,GAAG,qBAAgB;QACnB,GAAG,oBAAmB;QACtB,GAAG,oBAAkB;QACrB,GAAG,oBAAkB;QACrB,GAAG,oBAAuB;QAC1B,GAAG,oBAAoB;QACvB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;KACxB;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport {\n entities as UsersAuthProvidersEntities,\n resolvers as UsersAuthProvidersResolvers\n} from './users-auth-providers/index.js'\nimport { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider/index.js'\nimport { resolvers as AppbindingResolver } from './app-binding/index.js'\nimport { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance/index.js'\nimport { entities as ApplicationEntities, resolvers as ApplicationResolvers } from './application/index.js'\nimport { resolvers as DomainGeneratorResolver } from './domain-generator/index.js'\nimport { entities as GrantedRoleEntities, resolvers as GrantedRoleResolver } from './granted-role/index.js'\nimport { entities as InvitationEntities, resolvers as InvitationResolver } from './invitation/index.js'\nimport { entities as LoginHistoryEntities, resolvers as LoginHistoryResolver } from './login-history/index.js'\nimport { entities as PartnerEntities, resolvers as PartnerResolvers } from './partner/index.js'\nimport { entities as PasswordHistoryEntities } from './password-history/index.js'\nimport { entities as PrivilegeEntities, resolvers as PrivilegeResolvers } from './privilege/index.js'\nimport { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privilege/privilege-directive.js'\nimport { entities as RoleEntities, resolvers as RoleResolvers } from './role/index.js'\nimport { resolvers as RoleTemplateResolvers } from './role-template/index.js'\nimport { entities as UserEntities, resolvers as UserResolvers } from './user/index.js'\nimport { entities as VerificationTokenEntities } from './verification-token/index.js'\nimport { entities as WebAuthCredentialEntities } from './web-auth-credential/index.js'\nimport { entities as DomainLinkEntities, resolvers as DomainLinkResolver } from './domain-link/index.js'\nimport { entities as DomainOwnerEntities, resolvers as DomainOwnerResolvers } from './domain-owner/index.js'\n\n/* EXPORT ENTITY TYPES */\nexport * from './users-auth-providers/users-auth-providers.js'\nexport * from './auth-provider/auth-provider.js'\nexport * from './application/application.js'\nexport * from './appliance/appliance.js'\nexport * from './privilege/privilege.js'\nexport * from './role/role.js'\nexport * from './privilege/privilege-axis.js'\nexport * from './role-template/role-template.js'\nexport * from './role-template/role-template-types.js'\nexport * from './user/user.js'\nexport * from './partner/partner.js'\nexport * from './granted-role/granted-role.js'\nexport * from './invitation/invitation.js'\nexport * from './app-binding/app-binding.js'\nexport * from './password-history/password-history.js'\nexport * from './verification-token/verification-token.js'\nexport * from './login-history/login-history.js'\nexport * from './web-auth-credential/web-auth-credential.js'\nexport * from './domain-link/domain-link.js'\nexport * from './domain-owner/domain-owner.js'\n\n/* EXPORT TYPES */\nexport * from './app-binding/app-binding-types.js'\nexport * from './appliance/appliance-types.js'\nexport * from './application/application-types.js'\nexport * from './domain-generator/domain-generator-types.js'\nexport * from './invitation/invitation-types.js'\nexport * from './partner/partner-types.js'\nexport * from './privilege/privilege-types.js'\nexport * from './role/role-types.js'\nexport * from './user/user-types.js'\nexport * from './domain-link/domain-link-types.js'\n\nexport const entities = [\n /* ENTITIES */\n ...UsersAuthProvidersEntities,\n ...AuthProviderEntities,\n ...ApplicationEntities,\n ...ApplianceEntities,\n ...PrivilegeEntities,\n ...RoleEntities,\n ...UserEntities,\n ...PartnerEntities,\n ...GrantedRoleEntities,\n ...InvitationEntities,\n ...PasswordHistoryEntities,\n ...VerificationTokenEntities,\n ...LoginHistoryEntities,\n ...WebAuthCredentialEntities,\n ...DomainLinkEntities,\n ...DomainOwnerEntities\n]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs\n },\n\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...UsersAuthProvidersResolvers,\n ...AuthProviderResolvers,\n ...ApplicationResolvers,\n ...ApplianceResolvers,\n ...PrivilegeResolvers,\n ...RoleResolvers,\n ...RoleTemplateResolvers,\n ...UserResolvers,\n ...PartnerResolvers,\n ...GrantedRoleResolver,\n ...InvitationResolver,\n ...AppbindingResolver,\n ...DomainGeneratorResolver,\n ...LoginHistoryResolver,\n ...DomainLinkResolver,\n ...DomainOwnerResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * What an installation gets when no module says otherwise.
3
+ *
4
+ * The two travel together: a house running on the default vocabulary is also running on the
5
+ * default answer to "which of these is read-only". Splitting them would leave the common case
6
+ * — nobody declared anything — with a vocabulary and no idea which half is safe.
7
+ */
8
+ export declare const DEFAULT_AXES: string[];
9
+ export declare const DEFAULT_READ_ONLY_AXES: string[];
10
+ /**
11
+ * Declare the axes a module uses, and which of them are read-only. Additive.
12
+ *
13
+ * Additive rather than one installation-wide list because a single installation genuinely
14
+ * mixes vocabularies: in dssp, `project` is declared with query/mutation while `kpi` uses
15
+ * eight words of its own. Each module says what it uses, and says it in the same call that
16
+ * says which are read-only — a separate call for the second half could drift out of step with
17
+ * the first, and drift like that is silent.
18
+ *
19
+ * Declaring an axis that is already known is not an error. Two modules reaching for `read`
20
+ * independently is the case this is built for, not a conflict.
21
+ *
22
+ * ── `readOnly` means "holding only this axis changes nothing" ────────────────
23
+ * It does **not** mean `@Query`. The two come apart, and where they do the axis is right and
24
+ * the operation type is the loose one:
25
+ *
26
+ * @Mutation(returns => ExportResult)
27
+ * @Directive('@privilege(category: "label-studio", privilege: "query")')
28
+ * async exportLabelStudioAnnotations(…)
29
+ *
30
+ * That calls an external API's export and hands back what it returns. Nothing in our database
31
+ * moves. It is declared `@Mutation` and the axis is `query`, and **the axis is the correct
32
+ * one** — a role holding only `label-studio:query` still changes nothing by running it.
33
+ *
34
+ * Six declarations in this house disagree this way, and all six are deliberate. Do not
35
+ * "fix" them to match their operation type.
36
+ */
37
+ export declare function declareAxes(axes: string[], options?: {
38
+ readOnly?: string[];
39
+ }): void;
40
+ /**
41
+ * The axes this installation accepts.
42
+ *
43
+ * Falls back to the default rather than to "no check": an empty registry means nobody
44
+ * declared, not that everything is allowed.
45
+ */
46
+ export declare function declaredAxes(): Set<string>;
47
+ /** Whether a value may be used as an axis here. */
48
+ export declare function isDeclaredAxis(axis: string): boolean;
49
+ /**
50
+ * The axes that open nothing a role can change.
51
+ *
52
+ * Empty is a real answer, and it is different from the default. A house that declared its own
53
+ * vocabulary and never said which half is read-only has not told us — see
54
+ * `readOnlyAxesAreKnown`.
55
+ */
56
+ export declare function readOnlyAxes(): Set<string>;
57
+ /**
58
+ * Whether anyone has said which axes are read-only.
59
+ *
60
+ * `computedRoleTemplates` asks this before offering VIEWER. Guessing would mean matching the
61
+ * literal word `query`, which is what this replaces: it is right in this house and wrong in
62
+ * dssp, where the read axis is spelled `read`.
63
+ */
64
+ export declare function readOnlyAxesAreKnown(): boolean;
65
+ /**
66
+ * Say what the vocabulary ended up being.
67
+ *
68
+ * The list is not in the SDL and cannot be, since it differs per installation. A line at boot
69
+ * is the honest substitute — and it is the only way to tell "nobody declared, so these are the
70
+ * defaults" from "someone declared exactly these".
71
+ */
72
+ export declare function reportDeclaredAxes(): void;
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_READ_ONLY_AXES = exports.DEFAULT_AXES = void 0;
4
+ exports.declareAxes = declareAxes;
5
+ exports.declaredAxes = declaredAxes;
6
+ exports.isDeclaredAxis = isDeclaredAxis;
7
+ exports.readOnlyAxes = readOnlyAxes;
8
+ exports.readOnlyAxesAreKnown = readOnlyAxesAreKnown;
9
+ exports.reportDeclaredAxes = reportDeclaredAxes;
10
+ const env_1 = require("@things-factory/env");
11
+ /**
12
+ * The axis vocabulary this installation uses.
13
+ *
14
+ * ── ⚠ Call `declareAxes` at the top of the module's `server/index.ts` ────────
15
+ * Not from a bootstrap hook. The check runs while the schema is assembled, and the schema is
16
+ * assembled before every hook a module can reach:
17
+ *
18
+ * shell/server/server.ts:115 emitHook('bootstrap-module-subscription')
19
+ * shell/server/server.ts:117 await schema() ← the axis check runs in here
20
+ * shell/server/server.ts:295 emitHook('bootstrap-module-start')
21
+ *
22
+ * `bootstrap-module-start` is where a module registers most things, role templates included,
23
+ * and it is 178 lines too late for this one.
24
+ *
25
+ * ── What an axis is ──────────────────────────────────────────────────────────
26
+ * The `privilege:` half of `@privilege(category:, privilege:)`. It does **not** name a
27
+ * GraphQL operation. It names which of a category's gates a role has to hold, and the values
28
+ * this house uses happen to borrow transport words.
29
+ *
30
+ * ── Why the framework does not own the list ──────────────────────────────────
31
+ * Closing the axis to `query | mutation` was ruled and then withdrawn (architect,
32
+ * 2026-09-15). It was measured first:
33
+ *
34
+ * things-factory + operato-application 792 declarations, all query or mutation
35
+ * dssp 65 declarations, 40 of them neither
36
+ * kpi: read · input · assessment · sentiment
37
+ * finalize · recalculate · auto-collect
38
+ * basic-info
39
+ *
40
+ * A framework enum would have made dssp's upgrade a 40-declaration migration, and the cost
41
+ * would fall on people who were not in the room. The framework cannot know which words are
42
+ * right; it does not own the modules that use them.
43
+ *
44
+ * ── What this buys ───────────────────────────────────────────────────────────
45
+ * A typo stops being a silent gate. `@privilege(category: "board", privilege: "mutaiton")`
46
+ * used to register a pair in `process['PRIVILEGES']` that nobody would ever be granted — a
47
+ * door permanently shut, with no error anywhere.
48
+ */
49
+ const REGISTRY = 'PRIVILEGE_AXES';
50
+ /**
51
+ * What an installation gets when no module says otherwise.
52
+ *
53
+ * The two travel together: a house running on the default vocabulary is also running on the
54
+ * default answer to "which of these is read-only". Splitting them would leave the common case
55
+ * — nobody declared anything — with a vocabulary and no idea which half is safe.
56
+ */
57
+ exports.DEFAULT_AXES = ['query', 'mutation'];
58
+ exports.DEFAULT_READ_ONLY_AXES = ['query'];
59
+ function registry() {
60
+ if (!process[REGISTRY]) {
61
+ process[REGISTRY] = { axes: new Set(), readOnly: new Set(), declared: false };
62
+ }
63
+ return process[REGISTRY];
64
+ }
65
+ /**
66
+ * Declare the axes a module uses, and which of them are read-only. Additive.
67
+ *
68
+ * Additive rather than one installation-wide list because a single installation genuinely
69
+ * mixes vocabularies: in dssp, `project` is declared with query/mutation while `kpi` uses
70
+ * eight words of its own. Each module says what it uses, and says it in the same call that
71
+ * says which are read-only — a separate call for the second half could drift out of step with
72
+ * the first, and drift like that is silent.
73
+ *
74
+ * Declaring an axis that is already known is not an error. Two modules reaching for `read`
75
+ * independently is the case this is built for, not a conflict.
76
+ *
77
+ * ── `readOnly` means "holding only this axis changes nothing" ────────────────
78
+ * It does **not** mean `@Query`. The two come apart, and where they do the axis is right and
79
+ * the operation type is the loose one:
80
+ *
81
+ * @Mutation(returns => ExportResult)
82
+ * @Directive('@privilege(category: "label-studio", privilege: "query")')
83
+ * async exportLabelStudioAnnotations(…)
84
+ *
85
+ * That calls an external API's export and hands back what it returns. Nothing in our database
86
+ * moves. It is declared `@Mutation` and the axis is `query`, and **the axis is the correct
87
+ * one** — a role holding only `label-studio:query` still changes nothing by running it.
88
+ *
89
+ * Six declarations in this house disagree this way, and all six are deliberate. Do not
90
+ * "fix" them to match their operation type.
91
+ */
92
+ function declareAxes(axes, options = {}) {
93
+ if (!Array.isArray(axes) || axes.length === 0) {
94
+ throw new Error('declareAxes needs at least one axis. An empty list would refuse every declaration in ' +
95
+ 'the installation, which is never what the caller meant.');
96
+ }
97
+ for (const axis of axes) {
98
+ if (typeof axis !== 'string' || !axis.trim()) {
99
+ throw new Error(`declareAxes got ${JSON.stringify(axis)} as an axis. Axes are non-empty strings.`);
100
+ }
101
+ }
102
+ const readOnly = options.readOnly || [];
103
+ /*
104
+ * A read-only axis has to be one of the axes named in the same call.
105
+ *
106
+ * Otherwise a typo here is the quietest kind: `readOnly: ['querry']` would mark nothing,
107
+ * VIEWER would silently lose every category of this module, and the screen would count the
108
+ * shortfall correctly and show it as fact.
109
+ */
110
+ for (const axis of readOnly) {
111
+ if (!axes.includes(axis)) {
112
+ throw new Error(`declareAxes was told "${axis}" is read-only, but that axis is not in the same call. ` +
113
+ `Declared here: ${axes.join(', ')}.`);
114
+ }
115
+ }
116
+ const store = registry();
117
+ for (const axis of axes) {
118
+ store.axes.add(axis);
119
+ }
120
+ for (const axis of readOnly) {
121
+ store.readOnly.add(axis);
122
+ }
123
+ store.declared = true;
124
+ }
125
+ /**
126
+ * The axes this installation accepts.
127
+ *
128
+ * Falls back to the default rather than to "no check": an empty registry means nobody
129
+ * declared, not that everything is allowed.
130
+ */
131
+ function declaredAxes() {
132
+ const store = registry();
133
+ return store.declared ? store.axes : new Set(exports.DEFAULT_AXES);
134
+ }
135
+ /** Whether a value may be used as an axis here. */
136
+ function isDeclaredAxis(axis) {
137
+ return declaredAxes().has(axis);
138
+ }
139
+ /**
140
+ * The axes that open nothing a role can change.
141
+ *
142
+ * Empty is a real answer, and it is different from the default. A house that declared its own
143
+ * vocabulary and never said which half is read-only has not told us — see
144
+ * `readOnlyAxesAreKnown`.
145
+ */
146
+ function readOnlyAxes() {
147
+ const store = registry();
148
+ return store.declared ? store.readOnly : new Set(exports.DEFAULT_READ_ONLY_AXES);
149
+ }
150
+ /**
151
+ * Whether anyone has said which axes are read-only.
152
+ *
153
+ * `computedRoleTemplates` asks this before offering VIEWER. Guessing would mean matching the
154
+ * literal word `query`, which is what this replaces: it is right in this house and wrong in
155
+ * dssp, where the read axis is spelled `read`.
156
+ */
157
+ function readOnlyAxesAreKnown() {
158
+ return readOnlyAxes().size > 0;
159
+ }
160
+ /**
161
+ * Say what the vocabulary ended up being.
162
+ *
163
+ * The list is not in the SDL and cannot be, since it differs per installation. A line at boot
164
+ * is the honest substitute — and it is the only way to tell "nobody declared, so these are the
165
+ * defaults" from "someone declared exactly these".
166
+ */
167
+ function reportDeclaredAxes() {
168
+ const store = registry();
169
+ const axes = [...declaredAxes()].sort().join(', ');
170
+ const readOnly = [...readOnlyAxes()].sort().join(', ');
171
+ if (!store.declared) {
172
+ env_1.logger.info(`[privilege] no module declared privilege axes; using the defaults: ${axes} (read-only: ${readOnly})`);
173
+ return;
174
+ }
175
+ env_1.logger.info(`[privilege] axes declared by this installation: ${axes}`);
176
+ if (readOnlyAxesAreKnown()) {
177
+ env_1.logger.info(`[privilege] read-only axes: ${readOnly}`);
178
+ return;
179
+ }
180
+ /*
181
+ * Loud, because the consequence is a template quietly going missing from the role screen.
182
+ * "We do not know" is not "there are none", and folding the two would hand an administrator
183
+ * a VIEWER that opens nothing.
184
+ */
185
+ env_1.logger.warn('[privilege] no module said which axes are read-only, so the VIEWER template is not offered. ' +
186
+ "Add it where the axes are declared: declareAxes([...], { readOnly: ['query'] }).");
187
+ }
188
+ //# sourceMappingURL=privilege-axis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privilege-axis.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-axis.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger } from '@things-factory/env'\n\n/**\n * The axis vocabulary this installation uses.\n *\n * ── ⚠ Call `declareAxes` at the top of the module's `server/index.ts` ────────\n * Not from a bootstrap hook. The check runs while the schema is assembled, and the schema is\n * assembled before every hook a module can reach:\n *\n * shell/server/server.ts:115 emitHook('bootstrap-module-subscription')\n * shell/server/server.ts:117 await schema() ← the axis check runs in here\n * shell/server/server.ts:295 emitHook('bootstrap-module-start')\n *\n * `bootstrap-module-start` is where a module registers most things, role templates included,\n * and it is 178 lines too late for this one.\n *\n * ── What an axis is ──────────────────────────────────────────────────────────\n * The `privilege:` half of `@privilege(category:, privilege:)`. It does **not** name a\n * GraphQL operation. It names which of a category's gates a role has to hold, and the values\n * this house uses happen to borrow transport words.\n *\n * ── Why the framework does not own the list ──────────────────────────────────\n * Closing the axis to `query | mutation` was ruled and then withdrawn (architect,\n * 2026-09-15). It was measured first:\n *\n * things-factory + operato-application 792 declarations, all query or mutation\n * dssp 65 declarations, 40 of them neither\n * kpi: read · input · assessment · sentiment\n * finalize · recalculate · auto-collect\n * basic-info\n *\n * A framework enum would have made dssp's upgrade a 40-declaration migration, and the cost\n * would fall on people who were not in the room. The framework cannot know which words are\n * right; it does not own the modules that use them.\n *\n * ── What this buys ───────────────────────────────────────────────────────────\n * A typo stops being a silent gate. `@privilege(category: \"board\", privilege: \"mutaiton\")`\n * used to register a pair in `process['PRIVILEGES']` that nobody would ever be granted — a\n * door permanently shut, with no error anywhere.\n */\n\nconst REGISTRY = 'PRIVILEGE_AXES'\n\n/**\n * What an installation gets when no module says otherwise.\n *\n * The two travel together: a house running on the default vocabulary is also running on the\n * default answer to \"which of these is read-only\". Splitting them would leave the common case\n * — nobody declared anything — with a vocabulary and no idea which half is safe.\n */\nexport const DEFAULT_AXES = ['query', 'mutation']\nexport const DEFAULT_READ_ONLY_AXES = ['query']\n\ntype Registry = { axes: Set<string>; readOnly: Set<string>; declared: boolean }\n\nfunction registry(): Registry {\n if (!process[REGISTRY]) {\n process[REGISTRY] = { axes: new Set<string>(), readOnly: new Set<string>(), declared: false }\n }\n\n return process[REGISTRY]\n}\n\n/**\n * Declare the axes a module uses, and which of them are read-only. Additive.\n *\n * Additive rather than one installation-wide list because a single installation genuinely\n * mixes vocabularies: in dssp, `project` is declared with query/mutation while `kpi` uses\n * eight words of its own. Each module says what it uses, and says it in the same call that\n * says which are read-only — a separate call for the second half could drift out of step with\n * the first, and drift like that is silent.\n *\n * Declaring an axis that is already known is not an error. Two modules reaching for `read`\n * independently is the case this is built for, not a conflict.\n *\n * ── `readOnly` means \"holding only this axis changes nothing\" ────────────────\n * It does **not** mean `@Query`. The two come apart, and where they do the axis is right and\n * the operation type is the loose one:\n *\n * @Mutation(returns => ExportResult)\n * @Directive('@privilege(category: \"label-studio\", privilege: \"query\")')\n * async exportLabelStudioAnnotations(…)\n *\n * That calls an external API's export and hands back what it returns. Nothing in our database\n * moves. It is declared `@Mutation` and the axis is `query`, and **the axis is the correct\n * one** — a role holding only `label-studio:query` still changes nothing by running it.\n *\n * Six declarations in this house disagree this way, and all six are deliberate. Do not\n * \"fix\" them to match their operation type.\n */\nexport function declareAxes(axes: string[], options: { readOnly?: string[] } = {}): void {\n if (!Array.isArray(axes) || axes.length === 0) {\n throw new Error(\n 'declareAxes needs at least one axis. An empty list would refuse every declaration in ' +\n 'the installation, which is never what the caller meant.'\n )\n }\n\n for (const axis of axes) {\n if (typeof axis !== 'string' || !axis.trim()) {\n throw new Error(`declareAxes got ${JSON.stringify(axis)} as an axis. Axes are non-empty strings.`)\n }\n }\n\n const readOnly = options.readOnly || []\n\n /*\n * A read-only axis has to be one of the axes named in the same call.\n *\n * Otherwise a typo here is the quietest kind: `readOnly: ['querry']` would mark nothing,\n * VIEWER would silently lose every category of this module, and the screen would count the\n * shortfall correctly and show it as fact.\n */\n for (const axis of readOnly) {\n if (!axes.includes(axis)) {\n throw new Error(\n `declareAxes was told \"${axis}\" is read-only, but that axis is not in the same call. ` +\n `Declared here: ${axes.join(', ')}.`\n )\n }\n }\n\n const store = registry()\n\n for (const axis of axes) {\n store.axes.add(axis)\n }\n\n for (const axis of readOnly) {\n store.readOnly.add(axis)\n }\n\n store.declared = true\n}\n\n/**\n * The axes this installation accepts.\n *\n * Falls back to the default rather than to \"no check\": an empty registry means nobody\n * declared, not that everything is allowed.\n */\nexport function declaredAxes(): Set<string> {\n const store = registry()\n\n return store.declared ? store.axes : new Set(DEFAULT_AXES)\n}\n\n/** Whether a value may be used as an axis here. */\nexport function isDeclaredAxis(axis: string): boolean {\n return declaredAxes().has(axis)\n}\n\n/**\n * The axes that open nothing a role can change.\n *\n * Empty is a real answer, and it is different from the default. A house that declared its own\n * vocabulary and never said which half is read-only has not told us — see\n * `readOnlyAxesAreKnown`.\n */\nexport function readOnlyAxes(): Set<string> {\n const store = registry()\n\n return store.declared ? store.readOnly : new Set(DEFAULT_READ_ONLY_AXES)\n}\n\n/**\n * Whether anyone has said which axes are read-only.\n *\n * `computedRoleTemplates` asks this before offering VIEWER. Guessing would mean matching the\n * literal word `query`, which is what this replaces: it is right in this house and wrong in\n * dssp, where the read axis is spelled `read`.\n */\nexport function readOnlyAxesAreKnown(): boolean {\n return readOnlyAxes().size > 0\n}\n\n/**\n * Say what the vocabulary ended up being.\n *\n * The list is not in the SDL and cannot be, since it differs per installation. A line at boot\n * is the honest substitute — and it is the only way to tell \"nobody declared, so these are the\n * defaults\" from \"someone declared exactly these\".\n */\nexport function reportDeclaredAxes(): void {\n const store = registry()\n const axes = [...declaredAxes()].sort().join(', ')\n const readOnly = [...readOnlyAxes()].sort().join(', ')\n\n if (!store.declared) {\n logger.info(`[privilege] no module declared privilege axes; using the defaults: ${axes} (read-only: ${readOnly})`)\n return\n }\n\n logger.info(`[privilege] axes declared by this installation: ${axes}`)\n\n if (readOnlyAxesAreKnown()) {\n logger.info(`[privilege] read-only axes: ${readOnly}`)\n return\n }\n\n /*\n * Loud, because the consequence is a template quietly going missing from the role screen.\n * \"We do not know\" is not \"there are none\", and folding the two would hand an administrator\n * a VIEWER that opens nothing.\n */\n logger.warn(\n '[privilege] no module said which axes are read-only, so the VIEWER template is not offered. ' +\n \"Add it where the axes are declared: declareAxes([...], { readOnly: ['query'] }).\"\n )\n}\n"]}
@@ -1,49 +1,18 @@
1
1
  import { GraphQLSchema } from 'graphql';
2
2
  /**
3
- * `legacy` — **권한을 옮기는 동안 옛 이름도 받아 준다.**
3
+ * ⚠ **다음에 목록 인자를 붙이는 사람에게.**
4
4
  *
5
- * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
6
- * 리졸버의 `category` 를 바꾸는 순간 **역할이 들고 있던 기존 부여가 고아가 된다.** 새 이름을
7
- * 가진 사람이 아직 없으므로, 옮긴 그날 그 기능을 쓰던 사람들이 전부 막힌다.
5
+ * 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.
6
+ * **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼
7
+ * 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.
8
8
  *
9
- * 같은 자리가 두 곳에서 필요하다. 하나는 `board-ai` 의 비-보드 op 를 `ai-assistant` 로 옮기는
10
- * 일이고(공장 질문이 보드 편집 권한을 요구하던 문제), 다른 하나는 `execute` 축을 세울 때다
11
- * (`mutation` 을 들고 있던 역할이 승인 권한을 잃는다). 그래서 장치를 하나로 만든다.
9
+ * 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서
10
+ * 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**
11
+ * 요구해야 한다.
12
12
  *
13
- * ── 부여를 베끼지 않는다 ────────────────────────────────────────────────────
14
- * 옛 부여를 가진 역할에 새 권한을 써 넣는 길이 먼저 떠오르는데, 그것은 두 가지를 잃는다.
15
- * **조용하다** — 아무도 자기가 새 권한을 받은 것을 모른다. 그리고 **되돌릴 근거가 없어진다** —
16
- * 편집만 필요한 역할에서 나중에 그것을 뺄 때 누가 실제로 필요했는지 알 방법이 없다.
17
- *
18
- * 그래서 행은 건드리지 않고 **문에서 한동안 받아 준다.** 그러면 누가 이 문에 기대고 있는지
19
- * 로그에 쌓이고, 짐작이 아니라 **잰 것으로** 부여할 수 있다.
20
- *
21
- * ── 지울 시점은 눈에 보인다 ─────────────────────────────────────────────────
22
- * **옛 쌍은 `process['PRIVILEGES']` 에 등록하지 않는다.** 그래서 옛 권한 행이
23
- * `isDeprecatedPrivilege` 기준으로 유령이 되어 관리자 목록에 취소선으로 나타난다
24
- * (`privilege-deprecation.ts`). 별칭을 지울 때가 됐다는 신호를 새로 만들 필요가 없다.
25
- *
26
- * ⚠ **조건만으로 지우지 않는다.** 「N일간 legacy 통과 0건」은 그 업무를 아무도 안 한 조용한
27
- * 한 주로 충족된다. **고정 날짜**를 받아 그날 지운다.
28
- *
29
- * ── 푸는 순서 ───────────────────────────────────────────────────────────────
30
- * 지금은 둘이다. 모듈 기본값 시임이 서면 그 사이에 한 단이 더 들어간다
31
- * (`docs/design/auth-rebuild.md` §6-4).
32
- *
33
- * ```
34
- * 지금 필드 선언 → 필드의 legacy → 거절
35
- * 뒤에 필드 선언 → 필드의 legacy → 모듈 기본값 → DENY
36
- * ```
37
- *
38
- * ── ⚠ 다음에 목록 인자를 붙이는 사람에게 ────────────────────────────────────
39
- * **`legacy` 는 OR 다** — 새 이름이나 옛 이름, 어느 쪽이든 통과한다.
40
- *
41
- * 곧 필요해지는 것은 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서 당겨오면서
42
- * 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다** 요구해야 한다.
43
- *
44
- * 같은 모양의 인자가 정반대 뜻을 가지므로, 그것을 `privileges: [...]` 처럼 중립적으로 이름
45
- * 지으면 다음 사람이 「이 중 하나」로 읽는다. 그렇게 쓰는 날 게이트가 **오류 없이** 열린다.
46
- * 그러니 `requiresAll: [...]` 처럼 **이름이 AND 라고 말하게** 한다.
13
+ * 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,
14
+ * 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고
15
+ * 말하게** 한다.
47
16
  */
48
17
  export declare const privilegeDirectiveTypeDefs: import("graphql").DocumentNode;
49
18
  export declare const privilegeDirectiveResolver: (schema: GraphQLSchema) => GraphQLSchema;
@@ -5,56 +5,25 @@ const tslib_1 = require("tslib");
5
5
  const graphql_1 = require("graphql");
6
6
  const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
7
7
  const utils_1 = require("@graphql-tools/utils");
8
- const env_1 = require("@things-factory/env");
9
8
  const check_permission_js_1 = require("../../utils/check-permission.js");
9
+ const privilege_axis_js_1 = require("./privilege-axis.js");
10
10
  const privilege_rejection_js_1 = require("./privilege-rejection.js");
11
11
  process['PRIVILEGES'] = {};
12
12
  const DIRECTIVE = 'privilege';
13
13
  /**
14
- * `legacy` — **권한을 옮기는 동안 옛 이름도 받아 준다.**
14
+ * ⚠ **다음에 목록 인자를 붙이는 사람에게.**
15
15
  *
16
- * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
17
- * 리졸버의 `category` 를 바꾸는 순간 **역할이 들고 있던 기존 부여가 고아가 된다.** 새 이름을
18
- * 가진 사람이 아직 없으므로, 옮긴 그날 그 기능을 쓰던 사람들이 전부 막힌다.
16
+ * 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.
17
+ * **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼
18
+ * 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.
19
19
  *
20
- * 같은 자리가 두 곳에서 필요하다. 하나는 `board-ai` 의 비-보드 op 를 `ai-assistant` 로 옮기는
21
- * 일이고(공장 질문이 보드 편집 권한을 요구하던 문제), 다른 하나는 `execute` 축을 세울 때다
22
- * (`mutation` 을 들고 있던 역할이 승인 권한을 잃는다). 그래서 장치를 하나로 만든다.
20
+ * 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서
21
+ * 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**
22
+ * 요구해야 한다.
23
23
  *
24
- * ── 부여를 베끼지 않는다 ────────────────────────────────────────────────────
25
- * 옛 부여를 가진 역할에 새 권한을 써 넣는 길이 먼저 떠오르는데, 그것은 두 가지를 잃는다.
26
- * **조용하다** — 아무도 자기가 새 권한을 받은 것을 모른다. 그리고 **되돌릴 근거가 없어진다** —
27
- * 편집만 필요한 역할에서 나중에 그것을 뺄 때 누가 실제로 필요했는지 알 방법이 없다.
28
- *
29
- * 그래서 행은 건드리지 않고 **문에서 한동안 받아 준다.** 그러면 누가 이 문에 기대고 있는지
30
- * 로그에 쌓이고, 짐작이 아니라 **잰 것으로** 부여할 수 있다.
31
- *
32
- * ── 지울 시점은 눈에 보인다 ─────────────────────────────────────────────────
33
- * **옛 쌍은 `process['PRIVILEGES']` 에 등록하지 않는다.** 그래서 옛 권한 행이
34
- * `isDeprecatedPrivilege` 기준으로 유령이 되어 관리자 목록에 취소선으로 나타난다
35
- * (`privilege-deprecation.ts`). 별칭을 지울 때가 됐다는 신호를 새로 만들 필요가 없다.
36
- *
37
- * ⚠ **조건만으로 지우지 않는다.** 「N일간 legacy 통과 0건」은 그 업무를 아무도 안 한 조용한
38
- * 한 주로 충족된다. **고정 날짜**를 받아 그날 지운다.
39
- *
40
- * ── 푸는 순서 ───────────────────────────────────────────────────────────────
41
- * 지금은 둘이다. 모듈 기본값 시임이 서면 그 사이에 한 단이 더 들어간다
42
- * (`docs/design/auth-rebuild.md` §6-4).
43
- *
44
- * ```
45
- * 지금 필드 선언 → 필드의 legacy → 거절
46
- * 뒤에 필드 선언 → 필드의 legacy → 모듈 기본값 → DENY
47
- * ```
48
- *
49
- * ── ⚠ 다음에 목록 인자를 붙이는 사람에게 ────────────────────────────────────
50
- * **`legacy` 는 OR 다** — 새 이름이나 옛 이름, 어느 쪽이든 통과한다.
51
- *
52
- * 곧 필요해지는 것은 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서 당겨오면서
53
- * 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다** 요구해야 한다.
54
- *
55
- * 같은 모양의 인자가 정반대 뜻을 가지므로, 그것을 `privileges: [...]` 처럼 중립적으로 이름
56
- * 지으면 다음 사람이 「이 중 하나」로 읽는다. 그렇게 쓰는 날 게이트가 **오류 없이** 열린다.
57
- * 그러니 `requiresAll: [...]` 처럼 **이름이 AND 라고 말하게** 한다.
24
+ * 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,
25
+ * 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고
26
+ * 말하게** 한다.
58
27
  */
59
28
  exports.privilegeDirectiveTypeDefs = (0, graphql_tag_1.default) `
60
29
  directive @privilege(
@@ -62,7 +31,6 @@ exports.privilegeDirectiveTypeDefs = (0, graphql_tag_1.default) `
62
31
  privilege: String
63
32
  domainOwnerGranted: Boolean
64
33
  superUserGranted: Boolean
65
- legacy: String
66
34
  ) on FIELD_DEFINITION
67
35
  `;
68
36
  const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
@@ -73,19 +41,61 @@ const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
73
41
  if (!args) {
74
42
  throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`);
75
43
  }
76
- const { domainOwnerGranted, superUserGranted, category, privilege, legacy } = privilegeDirective;
77
- if (category && privilege) {
78
- process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
44
+ const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective;
45
+ const where = `${typeName}.${fieldName}`;
46
+ /*
47
+ * A declaration has to name something.
48
+ *
49
+ * `category` and `privilege` are one thing, not two: together they name a row in the
50
+ * privilege table, and either alone names no row, so `User.hasPrivilege` is never
51
+ * reached and the field falls back on ownership flags it did not set.
52
+ */
53
+ if (!category !== !privilege) {
54
+ throw new Error(`@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +
55
+ `The pair names the privilege to look for; one half names none.`);
56
+ }
57
+ /*
58
+ * `@privilege()` with nothing in it reads like a gate and guards nothing. Until
59
+ * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered
60
+ * false from a safe address and refused everyone, true from an unsafe one and let
61
+ * everyone through. Both are false now, so the shape is merely useless rather than
62
+ * dangerous — and a field that is open should say so by carrying no directive, not by
63
+ * carrying an empty one.
64
+ *
65
+ * It is also the shape someone reaches for wanting "being signed in is enough". That
66
+ * is a real thing to want and this directive does not express it; the answer settled
67
+ * on 2026-09-15 is a category for the published side of a module, `<module>-published`.
68
+ *
69
+ * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled
70
+ * for a resolver that declares nothing. A decorator that declares nothing is that
71
+ * same resolver wearing one.
72
+ */
73
+ if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {
74
+ throw new Error(`@privilege on ${where} declares nothing. ` +
75
+ `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +
76
+ `ownership gate. A field open to everyone carries no directive at all.`);
79
77
  }
80
78
  /*
81
- * 옛 쌍은 **일부러 등록하지 않는다** — 등록하면 옛 행이 유령으로 표시되지 않아
82
- * 별칭을 지울 시점을 아무도 못 본다. 위 주석 참조.
79
+ * The axis has to be one this installation declared.
80
+ *
81
+ * Without this, `privilege: "mutaiton"` registered a pair nobody would ever be granted
82
+ * — a gate shut forever, with no error to read. The list is not the framework's: see
83
+ * `privilege-axis.ts` for why, and for the fact that it must be declared at module
84
+ * import time rather than on a bootstrap hook.
83
85
  */
86
+ if (privilege && !(0, privilege_axis_js_1.isDeclaredAxis)(privilege)) {
87
+ throw new Error(`@privilege on ${where} uses axis "${privilege}", which this installation does not declare. ` +
88
+ `Declared axes: ${[...(0, privilege_axis_js_1.declaredAxes)()].sort().join(', ')}. ` +
89
+ `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +
90
+ `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`);
91
+ }
92
+ if (category && privilege) {
93
+ process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
94
+ }
84
95
  // 필드의 기존 description 가져오기
85
96
  const existingDescription = fieldConfig.description || '';
86
97
  // 권한 정보를 포함한 새로운 description 생성
87
98
  const privilegeDescription = `\n\n🔒 Requires privilege: ${category}:${privilege}` +
88
- (legacy ? ` (or ${legacy}:${privilege} while it is being moved)` : '') +
89
99
  (domainOwnerGranted ? ', Domain ownership' : '') +
90
100
  (superUserGranted ? ', System ownership' : '');
91
101
  // 기존 description과 결합
@@ -102,33 +112,11 @@ const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
102
112
  return await resolve.call(this, source, args, context, info);
103
113
  }
104
114
  /*
105
- * 새 이름으로 안 열렸다 — 옮기는 중이면 옛 이름으로 한 번 더 본다.
115
+ * 안 열렸으면 **거기서 끝이다.**
106
116
  *
107
- * 소유권 우회는 다시 묻지 않는다(`owner`·`super` 를 넘기지 않는다). 그것은 이미 위에서
108
- * 판정됐고, 여기서 다시 주면 옛 이름이 우회를 한 번 더 얻는 것이 된다.
117
+ * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지
118
+ * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.
109
119
  *
110
- * **금지 목록에 새 쌍이 있으면 옛 이름도 보지 않는다.** 금지는 관리자가 「이 IP 에서는
111
- * 이것을 못 한다」고 명시적으로 적어 둔 것인데, 금지 목록은 옛 이름을 모른다. 별칭이
112
- * 금지를 되돌리면 그 설정이 조용히 무력해진다.
113
- */
114
- const prohibited = (prohibitedPrivileges || []).some(pp => pp.category == category && pp.privilege == privilege);
115
- if (legacy && category && privilege && !prohibited) {
116
- const byLegacy = await (0, check_permission_js_1.checkPermission)({ category: legacy, privilege }, user, domain, unsafeIP, prohibitedPrivileges);
117
- if (byLegacy) {
118
- /*
119
- * **누가 이 문에 기대고 있나.** 이 목록이 부여의 근거가 된다 — 짐작으로 얹지 않는다.
120
- *
121
- * 어느 역할이 그것을 줬는지는 남기지 않는다. 그것을 알려면 매 요청 부여 경로를
122
- * 되짚는 질의가 하나 더 붙고, 여기는 모든 요청이 지나는 자리다. 사람과 리졸버가
123
- * 있으면 관리자가 역할 화면에서 찾을 수 있다.
124
- */
125
- env_1.logger.warn(`privilege moved: ${typeName}.${fieldName} passed on the legacy name ` +
126
- `${legacy}:${privilege} (new name ${category}:${privilege}) for ${user?.email || user?.id}`);
127
- return await resolve.call(this, source, args, context, info);
128
- }
129
- }
130
- /*
131
- * 거절은 **새 이름**으로 말한다 — 옛 이름을 말하면 관리자가 사라질 권한을 부여한다.
132
120
  * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.
133
121
  */
134
122
  throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege);