@things-factory/auth-base 10.1.79 → 10.1.107

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 (71) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/ai-entry/access-assistant.d.ts +50 -0
  3. package/dist-server/ai-entry/access-assistant.js +145 -0
  4. package/dist-server/ai-entry/access-assistant.js.map +1 -0
  5. package/dist-server/ai-entry/access-entry-targets.d.ts +32 -0
  6. package/dist-server/ai-entry/access-entry-targets.js +196 -0
  7. package/dist-server/ai-entry/access-entry-targets.js.map +1 -0
  8. package/dist-server/ai-entry/entry-target-shape.d.ts +82 -0
  9. package/dist-server/ai-entry/entry-target-shape.js +3 -0
  10. package/dist-server/ai-entry/entry-target-shape.js.map +1 -0
  11. package/dist-server/controllers/access-request.d.ts +57 -0
  12. package/dist-server/controllers/access-request.js +373 -0
  13. package/dist-server/controllers/access-request.js.map +1 -0
  14. package/dist-server/index.d.ts +3 -0
  15. package/dist-server/index.js +3 -0
  16. package/dist-server/index.js.map +1 -1
  17. package/dist-server/service/access-request/access-request-mutation.d.ts +17 -0
  18. package/dist-server/service/access-request/access-request-mutation.js +65 -0
  19. package/dist-server/service/access-request/access-request-mutation.js.map +1 -0
  20. package/dist-server/service/access-request/access-request-query.d.ts +16 -0
  21. package/dist-server/service/access-request/access-request-query.js +100 -0
  22. package/dist-server/service/access-request/access-request-query.js.map +1 -0
  23. package/dist-server/service/access-request/access-request-types.d.ts +32 -0
  24. package/dist-server/service/access-request/access-request-types.js +115 -0
  25. package/dist-server/service/access-request/access-request-types.js.map +1 -0
  26. package/dist-server/service/access-request/access-request.d.ts +33 -0
  27. package/dist-server/service/access-request/access-request.js +160 -0
  28. package/dist-server/service/access-request/access-request.js.map +1 -0
  29. package/dist-server/service/access-request/index.d.ts +5 -0
  30. package/dist-server/service/access-request/index.js +9 -0
  31. package/dist-server/service/access-request/index.js.map +1 -0
  32. package/dist-server/service/index.d.ts +3 -1
  33. package/dist-server/service/index.js +42 -37
  34. package/dist-server/service/index.js.map +1 -1
  35. package/dist-server/service/open-fields.js +5 -1
  36. package/dist-server/service/open-fields.js.map +1 -1
  37. package/dist-server/service/privilege/privilege-directive.js +6 -6
  38. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  39. package/dist-server/service/privilege/privilege-rejection.d.ts +10 -2
  40. package/dist-server/service/privilege/privilege-rejection.js +23 -10
  41. package/dist-server/service/privilege/privilege-rejection.js.map +1 -1
  42. package/dist-server/service/user/user-mutation.js +5 -4
  43. package/dist-server/service/user/user-mutation.js.map +1 -1
  44. package/dist-server/service/user/user-query.js +5 -17
  45. package/dist-server/service/user/user-query.js.map +1 -1
  46. package/dist-server/tsconfig.tsbuildinfo +1 -1
  47. package/dist-server/utils/access-request-review.d.ts +69 -0
  48. package/dist-server/utils/access-request-review.js +119 -0
  49. package/dist-server/utils/access-request-review.js.map +1 -0
  50. package/dist-server/utils/assignable-roles.d.ts +23 -0
  51. package/dist-server/utils/assignable-roles.js +89 -0
  52. package/dist-server/utils/assignable-roles.js.map +1 -0
  53. package/dist-server/utils/check-permission.d.ts +29 -2
  54. package/dist-server/utils/check-permission.js +48 -14
  55. package/dist-server/utils/check-permission.js.map +1 -1
  56. package/package.json +4 -4
  57. package/tests/access-assistant-db.test.ts +155 -0
  58. package/tests/access-entry-targets-db.test.ts +138 -0
  59. package/tests/access-request-db.test.ts +273 -0
  60. package/tests/access-request-pg.test.ts +72 -0
  61. package/tests/access-request-review.test.ts +155 -0
  62. package/tests/assignable-roles-db.test.ts +141 -0
  63. package/tests/permission-gate.test.ts +5 -7
  64. package/tests/privilege-directive.test.ts +39 -0
  65. package/tests/privilege-rejection.test.ts +32 -0
  66. package/tests/refusal-cause.test.ts +80 -0
  67. package/translations/en.json +28 -1
  68. package/translations/ja.json +28 -1
  69. package/translations/ko.json +28 -1
  70. package/translations/ms.json +28 -1
  71. package/translations/zh.json +28 -1
@@ -3,27 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.schema = exports.entities = exports.privilegeOfField = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  /* IMPORT ENTITIES AND RESOLVERS */
6
- const index_js_1 = require("./users-auth-providers/index.js");
7
- const index_js_2 = require("./auth-provider/index.js");
8
- const index_js_3 = require("./app-binding/index.js");
9
- const index_js_4 = require("./appliance/index.js");
10
- const index_js_5 = require("./application/index.js");
11
- const index_js_6 = require("./domain-generator/index.js");
12
- const index_js_7 = require("./granted-role/index.js");
13
- const index_js_8 = require("./invitation/index.js");
14
- const index_js_9 = require("./login-history/index.js");
15
- const index_js_10 = require("./partner/index.js");
16
- const index_js_11 = require("./password-history/index.js");
17
- const index_js_12 = require("./privilege/index.js");
6
+ const index_js_1 = require("./access-request/index.js");
7
+ const index_js_2 = require("./users-auth-providers/index.js");
8
+ const index_js_3 = require("./auth-provider/index.js");
9
+ const index_js_4 = require("./app-binding/index.js");
10
+ const index_js_5 = require("./appliance/index.js");
11
+ const index_js_6 = require("./application/index.js");
12
+ const index_js_7 = require("./domain-generator/index.js");
13
+ const index_js_8 = require("./granted-role/index.js");
14
+ const index_js_9 = require("./invitation/index.js");
15
+ const index_js_10 = require("./login-history/index.js");
16
+ const index_js_11 = require("./partner/index.js");
17
+ const index_js_12 = require("./password-history/index.js");
18
+ const index_js_13 = require("./privilege/index.js");
18
19
  const privilege_directive_js_1 = require("./privilege/privilege-directive.js");
19
20
  const open_fields_js_1 = require("./open-fields.js");
20
- const index_js_13 = require("./role/index.js");
21
- const index_js_14 = require("./role-template/index.js");
22
- const index_js_15 = require("./user/index.js");
23
- const index_js_16 = require("./verification-token/index.js");
24
- const index_js_17 = require("./web-auth-credential/index.js");
25
- const index_js_18 = require("./domain-link/index.js");
26
- const index_js_19 = require("./domain-owner/index.js");
21
+ const index_js_14 = require("./role/index.js");
22
+ const index_js_15 = require("./role-template/index.js");
23
+ const index_js_16 = require("./user/index.js");
24
+ const index_js_17 = require("./verification-token/index.js");
25
+ const index_js_18 = require("./web-auth-credential/index.js");
26
+ const index_js_19 = require("./domain-link/index.js");
27
+ const index_js_20 = require("./domain-owner/index.js");
27
28
  /* EXPORT ENTITY TYPES */
28
29
  tslib_1.__exportStar(require("./users-auth-providers/users-auth-providers.js"), exports);
29
30
  tslib_1.__exportStar(require("./auth-provider/auth-provider.js"), exports);
@@ -48,6 +49,7 @@ tslib_1.__exportStar(require("./login-history/login-history.js"), exports);
48
49
  tslib_1.__exportStar(require("./web-auth-credential/web-auth-credential.js"), exports);
49
50
  tslib_1.__exportStar(require("./domain-link/domain-link.js"), exports);
50
51
  tslib_1.__exportStar(require("./domain-owner/domain-owner.js"), exports);
52
+ tslib_1.__exportStar(require("./access-request/access-request.js"), exports);
51
53
  /* EXPORT TYPES */
52
54
  tslib_1.__exportStar(require("./app-binding/app-binding-types.js"), exports);
53
55
  tslib_1.__exportStar(require("./appliance/appliance-types.js"), exports);
@@ -59,24 +61,26 @@ tslib_1.__exportStar(require("./privilege/privilege-types.js"), exports);
59
61
  tslib_1.__exportStar(require("./role/role-types.js"), exports);
60
62
  tslib_1.__exportStar(require("./user/user-types.js"), exports);
61
63
  tslib_1.__exportStar(require("./domain-link/domain-link-types.js"), exports);
64
+ tslib_1.__exportStar(require("./access-request/access-request-types.js"), exports);
62
65
  exports.entities = [
63
66
  /* ENTITIES */
64
- ...index_js_1.entities,
65
67
  ...index_js_2.entities,
68
+ ...index_js_3.entities,
69
+ ...index_js_6.entities,
66
70
  ...index_js_5.entities,
67
- ...index_js_4.entities,
68
- ...index_js_12.entities,
69
71
  ...index_js_13.entities,
70
- ...index_js_15.entities,
71
- ...index_js_10.entities,
72
- ...index_js_7.entities,
73
- ...index_js_8.entities,
74
- ...index_js_11.entities,
72
+ ...index_js_14.entities,
75
73
  ...index_js_16.entities,
74
+ ...index_js_11.entities,
75
+ ...index_js_8.entities,
76
76
  ...index_js_9.entities,
77
+ ...index_js_12.entities,
77
78
  ...index_js_17.entities,
79
+ ...index_js_10.entities,
78
80
  ...index_js_18.entities,
79
- ...index_js_19.entities
81
+ ...index_js_19.entities,
82
+ ...index_js_20.entities,
83
+ ...index_js_1.entities
80
84
  ];
81
85
  exports.schema = {
82
86
  typeDefs: {
@@ -86,22 +90,23 @@ exports.schema = {
86
90
  openFields: open_fields_js_1.openFields,
87
91
  resolverClasses: [
88
92
  /* RESOLVER CLASSES */
89
- ...index_js_1.resolvers,
90
93
  ...index_js_2.resolvers,
94
+ ...index_js_3.resolvers,
95
+ ...index_js_6.resolvers,
91
96
  ...index_js_5.resolvers,
92
- ...index_js_4.resolvers,
93
- ...index_js_12.resolvers,
94
97
  ...index_js_13.resolvers,
95
98
  ...index_js_14.resolvers,
96
99
  ...index_js_15.resolvers,
97
- ...index_js_10.resolvers,
98
- ...index_js_7.resolvers,
100
+ ...index_js_16.resolvers,
101
+ ...index_js_11.resolvers,
99
102
  ...index_js_8.resolvers,
100
- ...index_js_3.resolvers,
101
- ...index_js_6.resolvers,
102
103
  ...index_js_9.resolvers,
103
- ...index_js_18.resolvers,
104
- ...index_js_19.resolvers
104
+ ...index_js_4.resolvers,
105
+ ...index_js_7.resolvers,
106
+ ...index_js_10.resolvers,
107
+ ...index_js_19.resolvers,
108
+ ...index_js_20.resolvers,
109
+ ...index_js_1.resolvers
105
110
  ],
106
111
  directives: {
107
112
  privilege: privilege_directive_js_1.privilegeDirectiveResolver
@@ -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,qDAA6C;AAC7C,+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,gHAAgH;AAChH,6EAAqF;AAA5E,0HAAA,gBAAgB,OAAA;AAGzB,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,6FAA6F;IAC7F,UAAU,EAAV,2BAAU;IAEV,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 { openFields } from './open-fields.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'\n/* What a field asks for, by name — for another entrance to the same door (operato-twin ADR-0072 decision 5). */\nexport { privilegeOfField, type FieldGate } from './privilege/privilege-directive.js'\n/* How a module declares the fields it leaves open — see `privilege/open-field-rule.ts`. */\nexport { type OpenFieldsDeclaration } from './privilege/open-field-rule.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 /* The fields that carry no @privilege, and why — `open-fields.ts`, read by the directive. */\n openFields,\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"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,wDAAkH;AAClH,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,oDAAuG;AACvG,wDAA8G;AAC9G,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,qDAA6C;AAC7C,+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,gHAAgH;AAChH,6EAAqF;AAA5E,0HAAA,gBAAgB,OAAA;AAGzB,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;AAC9C,6EAAkD;AAElD,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,2EAAgD;AAChD,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAClD,mFAAwD;AAE3C,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,oBAAoB;IACvB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;IACtB,GAAG,mBAAqB;CACzB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;KAC3B;IAED,6FAA6F;IAC7F,UAAU,EAAV,2BAAU;IAEV,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,qBAAoB;QACvB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;QACvB,GAAG,oBAAsB;KAC1B;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as AccessRequestEntities, resolvers as AccessRequestResolvers } from './access-request/index.js'\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 { openFields } from './open-fields.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'\n/* What a field asks for, by name — for another entrance to the same door (operato-twin ADR-0072 decision 5). */\nexport { privilegeOfField, type FieldGate } from './privilege/privilege-directive.js'\n/* How a module declares the fields it leaves open — see `privilege/open-field-rule.ts`. */\nexport { type OpenFieldsDeclaration } from './privilege/open-field-rule.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'\nexport * from './access-request/access-request.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'\nexport * from './access-request/access-request-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 ...AccessRequestEntities\n]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs\n },\n\n /* The fields that carry no @privilege, and why — `open-fields.ts`, read by the directive. */\n openFields,\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 ...AccessRequestResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
@@ -18,7 +18,11 @@ exports.openFields = {
18
18
  'Query.checkResettablePasswordToDefault': "about the caller's own password",
19
19
  'Query.checkDefaultPassword': 'whether the caller is still on the default password',
20
20
  'Query.passwordRule': "the installation's password policy. The change-password screen needs it before the caller has any privilege — and after a forced reset they may have none",
21
- 'Query.authProviderTypes': "which provider types this build supports. A catalogue, not a tenant's configuration"
21
+ 'Query.authProviderTypes': "which provider types this build supports. A catalogue, not a tenant's configuration",
22
+ 'Query.myAccessRequests': "the caller's own access requests",
23
+ 'Query.accessRequestDeciderCount': 'how many people an access request from the caller would reach - a number, never who',
24
+ 'Mutation.requestAccess': 'asked by someone refused for want of a role - a gate here would need the privilege being asked for. The door asks only for the caller and judges again that a role would open it (ADR-0103)',
25
+ 'Mutation.withdrawAccessRequest': "withdraws the caller's own pending request and nobody else's"
22
26
  }
23
27
  };
24
28
  //# sourceMappingURL=open-fields.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"open-fields.js","sourceRoot":"","sources":["../../server/service/open-fields.ts"],"names":[],"mappings":";;;AAEA;;;;;;GAMG;AACU,QAAA,UAAU,GAA0B;IAC/C,QAAQ,EAAE;QACR,wBAAwB,EAAE,2BAA2B;QACrD,eAAe,EAAE,wBAAwB;QACzC,oBAAoB,EAAE,0EAA0E;QAChG,4BAA4B,EAAE,sCAAsC;QACpE,8BAA8B,EAAE,2CAA2C;QAC3E,wCAAwC,EAAE,iCAAiC;QAC3E,4BAA4B,EAAE,qDAAqD;QACnF,oBAAoB,EAClB,2JAA2J;QAC7J,yBAAyB,EAAE,qFAAqF;KACjH;CACF,CAAA","sourcesContent":["import type { OpenFieldsDeclaration } from './privilege/open-field-rule.js'\n\n/**\n * auth-base's root fields that declare no `@privilege` — every one of them, and why.\n *\n * There is no `notYetDeclared` here. The twelve that were open without a reason were closed on\n * 2026-09-20 (`c02ee9bdc1`); what is left answers about the caller, or about the installation\n * rather than anybody's data. An addition to this list needs its reason written first.\n */\nexport const openFields: OpenFieldsDeclaration = {\n byDesign: {\n 'Query.myLoginHistories': \"the caller's own sign-ins\",\n 'Query.myRoles': \"the caller's own roles\",\n 'Query.hasPrivilege': 'whether the caller has one — a gate here would need the answer to itself',\n 'Query.domainsWithPrivilege': 'the domains where the caller has one',\n 'Query.checkUserBelongsDomain': 'whether the caller belongs to this domain',\n 'Query.checkResettablePasswordToDefault': \"about the caller's own password\",\n 'Query.checkDefaultPassword': 'whether the caller is still on the default password',\n 'Query.passwordRule':\n \"the installation's password policy. The change-password screen needs it before the caller has any privilege — and after a forced reset they may have none\",\n 'Query.authProviderTypes': \"which provider types this build supports. A catalogue, not a tenant's configuration\"\n }\n}\n"]}
1
+ {"version":3,"file":"open-fields.js","sourceRoot":"","sources":["../../server/service/open-fields.ts"],"names":[],"mappings":";;;AAEA;;;;;;GAMG;AACU,QAAA,UAAU,GAA0B;IAC/C,QAAQ,EAAE;QACR,wBAAwB,EAAE,2BAA2B;QACrD,eAAe,EAAE,wBAAwB;QACzC,oBAAoB,EAAE,0EAA0E;QAChG,4BAA4B,EAAE,sCAAsC;QACpE,8BAA8B,EAAE,2CAA2C;QAC3E,wCAAwC,EAAE,iCAAiC;QAC3E,4BAA4B,EAAE,qDAAqD;QACnF,oBAAoB,EAClB,2JAA2J;QAC7J,yBAAyB,EAAE,qFAAqF;QAChH,wBAAwB,EAAE,kCAAkC;QAC5D,iCAAiC,EAAE,qFAAqF;QACxH,wBAAwB,EACtB,6LAA6L;QAC/L,gCAAgC,EAAE,8DAA8D;KACjG;CACF,CAAA","sourcesContent":["import type { OpenFieldsDeclaration } from './privilege/open-field-rule.js'\n\n/**\n * auth-base's root fields that declare no `@privilege` — every one of them, and why.\n *\n * There is no `notYetDeclared` here. The twelve that were open without a reason were closed on\n * 2026-09-20 (`c02ee9bdc1`); what is left answers about the caller, or about the installation\n * rather than anybody's data. An addition to this list needs its reason written first.\n */\nexport const openFields: OpenFieldsDeclaration = {\n byDesign: {\n 'Query.myLoginHistories': \"the caller's own sign-ins\",\n 'Query.myRoles': \"the caller's own roles\",\n 'Query.hasPrivilege': 'whether the caller has one — a gate here would need the answer to itself',\n 'Query.domainsWithPrivilege': 'the domains where the caller has one',\n 'Query.checkUserBelongsDomain': 'whether the caller belongs to this domain',\n 'Query.checkResettablePasswordToDefault': \"about the caller's own password\",\n 'Query.checkDefaultPassword': 'whether the caller is still on the default password',\n 'Query.passwordRule':\n \"the installation's password policy. The change-password screen needs it before the caller has any privilege — and after a forced reset they may have none\",\n 'Query.authProviderTypes': \"which provider types this build supports. A catalogue, not a tenant's configuration\",\n 'Query.myAccessRequests': \"the caller's own access requests\",\n 'Query.accessRequestDeciderCount': 'how many people an access request from the caller would reach - a number, never who',\n 'Mutation.requestAccess':\n 'asked by someone refused for want of a role - a gate here would need the privilege being asked for. The door asks only for the caller and judges again that a role would open it (ADR-0103)',\n 'Mutation.withdrawAccessRequest': \"withdraws the caller's own pending request and nobody else's\"\n }\n}\n"]}
@@ -168,13 +168,13 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
168
168
  fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim();
169
169
  fieldConfig.resolve = async function (source, args, context, info) {
170
170
  const { domain, user, unsafeIP, prohibitedPrivileges } = context.state;
171
- const permitted = await (0, check_permission_js_1.checkPermission)({
171
+ const judgement = await (0, check_permission_js_1.judgePermission)({
172
172
  category,
173
173
  privilege,
174
174
  owner: domainOwnerGranted,
175
175
  super: superUserGranted
176
176
  }, user, domain, unsafeIP, prohibitedPrivileges);
177
- if (permitted) {
177
+ if (judgement.permitted === true) {
178
178
  return await resolve.call(this, source, args, context, info);
179
179
  }
180
180
  /*
@@ -185,7 +185,7 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
185
185
  *
186
186
  * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.
187
187
  */
188
- throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege);
188
+ throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege, judgement.because, info?.operation?.operation);
189
189
  };
190
190
  /*
191
191
  * ── A subscription is opened by `subscribe`, not `resolve` ────────────
@@ -197,9 +197,9 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
197
197
  const subscribe = fieldConfig.subscribe;
198
198
  fieldConfig.subscribe = async function (source, args, context, info) {
199
199
  const { domain, user, unsafeIP, prohibitedPrivileges } = context.state;
200
- const permitted = await (0, check_permission_js_1.checkPermission)({ category, privilege, owner: domainOwnerGranted, super: superUserGranted }, user, domain, unsafeIP, prohibitedPrivileges);
201
- if (!permitted) {
202
- throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege);
200
+ const judgement = await (0, check_permission_js_1.judgePermission)({ category, privilege, owner: domainOwnerGranted, super: superUserGranted }, user, domain, unsafeIP, prohibitedPrivileges);
201
+ if (judgement.permitted === false) {
202
+ throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege, judgement.because, 'subscription');
203
203
  }
204
204
  return await subscribe.call(this, source, args, context, info);
205
205
  };
@@ -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 { checkPermission } from '../../utils/check-permission.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\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 * 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\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 permitted = await checkPermission(\n {\n category,\n privilege,\n owner: domainOwnerGranted,\n super: superUserGranted\n },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n if (permitted) {\n return await resolve.call(this, source, args, context, info)\n }\n\n /*\n * 안 열렸으면 **거기서 끝이다.**\n *\n * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지\n * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.\n *\n * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.\n */\n throw privilegeRejection(context, category, privilege)\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 permitted = await checkPermission(\n { category, privilege, owner: domainOwnerGranted, super: superUserGranted },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n if (!permitted) {\n throw privilegeRejection(context, category, privilege)\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":";;;AAqDA,4CAIC;;AAzDD,qCAA2E;AAC3E,sEAA6B;AAE7B,gDAA0E;AAC1E,6CAA4C;AAE5C,yEAAiE;AACjE,2DAAkE;AAClE,qEAA6D;AAC7D,6DAO6B;AAE7B,OAAO,CAAC,YAAY,CAAC,GAAG,EAAE,CAAA;AAE1B;;;;;;;;;;;;;;;;;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;YAE5E,+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;;;;;;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;;;;;;;mBAOG;gBACH,IAAI,SAAS,IAAI,CAAC,IAAA,kCAAc,EAAC,SAAS,CAAC,EAAE,CAAC;oBAC5C,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,eAAe,SAAS,+CAA+C;wBAC3F,kBAAkB,CAAC,GAAG,IAAA,gCAAY,GAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;wBAC3D,2CAA2C,SAAS,uCAAuC;wBAC3F,oFAAoF,CACvF,CAAA;gBACH,CAAC;gBAED,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,CAAA;gBAEhD,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,IAAI,SAAS,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;wBACjC,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;oBAC9D,CAAC;oBAED;;;;;;;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;AA1MY,QAAA,0BAA0B,8BA0MtC;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 { 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\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 * 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\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 if (judgement.permitted === true) {\n return await resolve.call(this, source, args, context, info)\n }\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"]}
@@ -7,9 +7,17 @@ export type PrivilegeRequirement = {
7
7
  category: string;
8
8
  privilege: string;
9
9
  } | 'ownership';
10
+ /** Why it was refused - see `RefusalCause` in `utils/check-permission.ts`. Only `role` can be asked for (ADR-0103). */
11
+ export type { RefusalCause } from '../../utils/check-permission.js';
12
+ import type { RefusalCause } from '../../utils/check-permission.js';
10
13
  /**
11
- * 권한 거절 오류. `code: 'FORBIDDEN'` + `requires` 를 `extensions` 에 담는다.
14
+ * 권한 거절 오류. `code: 'FORBIDDEN'` + `requires` + `because` 를 `extensions` 에 담는다.
15
+ *
16
+ * `because` 는 화면이 「권한 요청」을 띄울지 정하는 칸이다 — 역할로 풀리는 것은 `role` 뿐이다.
17
+ * 부르는 쪽이 이유를 모르면(옛 호출) 이름이 있으면 `role`, 없으면 `ownership` 이다 — 지금까지의 뜻 그대로.
12
18
  *
13
19
  * @param context koa 컨텍스트(요청의 언어를 아는 `t` 를 가진다). 없어도 동작한다.
14
20
  */
15
- export declare function privilegeRejection(context: any, category?: string, privilege?: string): GraphQLError;
21
+ /** Which kind of operation was refused - a screen offers an access request only for a change the person made. */
22
+ export type RefusedOperation = 'query' | 'mutation' | 'subscription';
23
+ export declare function privilegeRejection(context: any, category?: string, privilege?: string, because?: RefusalCause, operation?: RefusedOperation): GraphQLError;
@@ -34,17 +34,30 @@ function localized(context, key, params, fallback) {
34
34
  */
35
35
  return !translated || translated === key ? fallback : translated;
36
36
  }
37
- /**
38
- * 권한 거절 오류. `code: 'FORBIDDEN'` + `requires` 를 `extensions` 에 담는다.
39
- *
40
- * @param context koa 컨텍스트(요청의 언어를 아는 `t` 를 가진다). 없어도 동작한다.
41
- */
42
- function privilegeRejection(context, category, privilege) {
37
+ /** The sentence for a named privilege, by why it was refused. `role` keeps the sentence it always had. */
38
+ const NAMED = {
39
+ role: { key: 'error.x privilege required', fallback: x => `unauthorized! ${x} privilege required` },
40
+ address: { key: 'error.x not allowed from this address', fallback: x => `unauthorized! ${x} is not allowed from this address` },
41
+ license: { key: 'error.x not licensed', fallback: x => `unauthorized! ${x} is not licensed on this installation` },
42
+ ownership: { key: 'error.x privilege required', fallback: x => `unauthorized! ${x} privilege required` },
43
+ unknown: { key: 'error.x refused for this request', fallback: x => `unauthorized! ${x} is refused for this request` }
44
+ };
45
+ function privilegeRejection(context, category, privilege, because, operation) {
43
46
  const named = !!(category && privilege);
44
47
  const requires = named ? { category, privilege } : 'ownership';
45
- const message = named
46
- ? localized(context, 'error.x privilege required', { x: `${category}:${privilege}` }, `unauthorized! ${category}:${privilege} privilege required`)
47
- : localized(context, 'error.ownership required', {}, 'unauthorized! domain or system ownership required');
48
- return new graphql_1.GraphQLError(message, { extensions: { code: 'FORBIDDEN', requires } });
48
+ const cause = because ?? (named ? 'role' : 'ownership');
49
+ let message;
50
+ if (named) {
51
+ const x = `${category}:${privilege}`;
52
+ const { key, fallback } = NAMED[cause] ?? NAMED.unknown;
53
+ message = localized(context, key, { x }, fallback(x));
54
+ }
55
+ else if (cause === 'address') {
56
+ message = localized(context, 'error.ownership not allowed from this address', {}, 'unauthorized! ownership does not count from this address');
57
+ }
58
+ else {
59
+ message = localized(context, 'error.ownership required', {}, 'unauthorized! domain or system ownership required');
60
+ }
61
+ return new graphql_1.GraphQLError(message, { extensions: { code: 'FORBIDDEN', requires, because: cause, ...(operation ? { operation } : {}) } });
49
62
  }
50
63
  //# sourceMappingURL=privilege-rejection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"privilege-rejection.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-rejection.ts"],"names":[],"mappings":";;AA+CA,gDAcC;AA7DD,qCAAsC;AAEtC;;;;;;;;;;;GAWG;AAEH,6BAA6B;AAC7B,SAAS,SAAS,CAAC,OAAY,EAAE,GAAW,EAAE,MAA8B,EAAE,QAAgB;IAC5F,MAAM,CAAC,GAAG,OAAO,OAAO,EAAE,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7D,IAAI,CAAC,CAAC;QAAE,OAAO,QAAQ,CAAA;IAEvB,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,UAAU,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,mDAAmD;QACnD,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,UAAU,IAAI,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAA;AAClE,CAAC;AAQD;;;;GAIG;AACH,SAAgB,kBAAkB,CAAC,OAAY,EAAE,QAAiB,EAAE,SAAkB;IACpF,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAA;IACvC,MAAM,QAAQ,GAAyB,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,WAAW,CAAA;IAEpF,MAAM,OAAO,GAAG,KAAK;QACnB,CAAC,CAAC,SAAS,CACP,OAAO,EACP,4BAA4B,EAC5B,EAAE,CAAC,EAAE,GAAG,QAAQ,IAAI,SAAS,EAAE,EAAE,EACjC,iBAAiB,QAAQ,IAAI,SAAS,qBAAqB,CAC5D;QACH,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,0BAA0B,EAAE,EAAE,EAAE,mDAAmD,CAAC,CAAA;IAE3G,OAAO,IAAI,sBAAY,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;AACnF,CAAC","sourcesContent":["import { GraphQLError } from 'graphql'\n\n/*\n * 권한 거절을 만든다 — **사용자의 말로, 코드와 함께.**\n *\n * ── 왜 따로 있나 ────────────────────────────────────────────────────────────\n * 이 문장은 프레임워크 apollo 링크(`@operato/graphql` 의 ERROR_HANDLER)를 타고 **사용자 얼굴까지** 간다\n * — 자동으로 닫히지 않는 오류 토스트다. 그런데 다른 서버 오류가 모두 `context.t` 로 옮겨진 뒤에도\n * 여기만 영어 한 문장(`Unauthorized! twin:mutation privilege required`)이었다.\n *\n * 지시자 안에 인라인으로 두면 테스트가 닿지 못한다(스키마를 짓고 리졸버를 태워야 한다). 위험한 것은\n * 게이트 판정이 아니라 **문장 만들기**다 — 번역기가 없는 컨텍스트, 키가 없는 언어, 형태에 의존하는\n * 소비처. 그래서 순수 함수로 떼어 둔다.\n */\n\n/** 번역이 없으면 영어 문장을 그대로 쓴다. */\nfunction localized(context: any, key: string, params: Record<string, string>, fallback: string): string {\n const t = typeof context?.t === 'function' ? context.t : null\n if (!t) return fallback\n\n let translated: string\n try {\n translated = t(key, params)\n } catch {\n /* 번역기가 던지면 오류의 원인이 번역기로 바뀐다 — 거절은 그대로 전달되어야 한다. */\n return fallback\n }\n\n /*\n * i18next 는 키를 못 찾으면 **키를 그대로 돌려준다.** 그걸 쓰면 사용자가\n * `error.x privilege required` 를 읽게 되므로 영어 문장으로 되돌린다\n * (같은 대비를 `privilege-query.ts` 의 description 도 한다).\n */\n return !translated || translated === key ? fallback : translated\n}\n\n/**\n * 무엇이 필요했는지 — 화면이 **문장을 다시 파싱하지 않고** 안내를 만들 수 있게 실어 보낸다.\n * 문장 형태에 의존하면 번역하는 순간 깨진다.\n */\nexport type PrivilegeRequirement = { category: string; privilege: string } | 'ownership'\n\n/**\n * 권한 거절 오류. `code: 'FORBIDDEN'` + `requires` 를 `extensions` 에 담는다.\n *\n * @param context koa 컨텍스트(요청의 언어를 아는 `t` 를 가진다). 없어도 동작한다.\n */\nexport function privilegeRejection(context: any, category?: string, privilege?: string): GraphQLError {\n const named = !!(category && privilege)\n const requires: PrivilegeRequirement = named ? { category, privilege } : 'ownership'\n\n const message = named\n ? localized(\n context,\n 'error.x privilege required',\n { x: `${category}:${privilege}` },\n `unauthorized! ${category}:${privilege} privilege required`\n )\n : localized(context, 'error.ownership required', {}, 'unauthorized! domain or system ownership required')\n\n return new GraphQLError(message, { extensions: { code: 'FORBIDDEN', requires } })\n}\n"]}
1
+ {"version":3,"file":"privilege-rejection.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-rejection.ts"],"names":[],"mappings":"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","sourcesContent":["import { GraphQLError } from 'graphql'\n\n/*\n * 권한 거절을 만든다 — **사용자의 말로, 코드와 함께.**\n *\n * ── 왜 따로 있나 ────────────────────────────────────────────────────────────\n * 이 문장은 프레임워크 apollo 링크(`@operato/graphql` 의 ERROR_HANDLER)를 타고 **사용자 얼굴까지** 간다\n * — 자동으로 닫히지 않는 오류 토스트다. 그런데 다른 서버 오류가 모두 `context.t` 로 옮겨진 뒤에도\n * 여기만 영어 한 문장(`Unauthorized! twin:mutation privilege required`)이었다.\n *\n * 지시자 안에 인라인으로 두면 테스트가 닿지 못한다(스키마를 짓고 리졸버를 태워야 한다). 위험한 것은\n * 게이트 판정이 아니라 **문장 만들기**다 — 번역기가 없는 컨텍스트, 키가 없는 언어, 형태에 의존하는\n * 소비처. 그래서 순수 함수로 떼어 둔다.\n */\n\n/** 번역이 없으면 영어 문장을 그대로 쓴다. */\nfunction localized(context: any, key: string, params: Record<string, string>, fallback: string): string {\n const t = typeof context?.t === 'function' ? context.t : null\n if (!t) return fallback\n\n let translated: string\n try {\n translated = t(key, params)\n } catch {\n /* 번역기가 던지면 오류의 원인이 번역기로 바뀐다 — 거절은 그대로 전달되어야 한다. */\n return fallback\n }\n\n /*\n * i18next 는 키를 못 찾으면 **키를 그대로 돌려준다.** 그걸 쓰면 사용자가\n * `error.x privilege required` 를 읽게 되므로 영어 문장으로 되돌린다\n * (같은 대비를 `privilege-query.ts` 의 description 도 한다).\n */\n return !translated || translated === key ? fallback : translated\n}\n\n/**\n * 무엇이 필요했는지 — 화면이 **문장을 다시 파싱하지 않고** 안내를 만들 수 있게 실어 보낸다.\n * 문장 형태에 의존하면 번역하는 순간 깨진다.\n */\nexport type PrivilegeRequirement = { category: string; privilege: string } | 'ownership'\n\n/** Why it was refused - see `RefusalCause` in `utils/check-permission.ts`. Only `role` can be asked for (ADR-0103). */\nexport type { RefusalCause } from '../../utils/check-permission.js'\nimport type { RefusalCause } from '../../utils/check-permission.js'\n\n/** The sentence for a named privilege, by why it was refused. `role` keeps the sentence it always had. */\nconst NAMED: Record<RefusalCause, { key: string; fallback: (x: string) => string }> = {\n role: { key: 'error.x privilege required', fallback: x => `unauthorized! ${x} privilege required` },\n address: { key: 'error.x not allowed from this address', fallback: x => `unauthorized! ${x} is not allowed from this address` },\n license: { key: 'error.x not licensed', fallback: x => `unauthorized! ${x} is not licensed on this installation` },\n ownership: { key: 'error.x privilege required', fallback: x => `unauthorized! ${x} privilege required` },\n unknown: { key: 'error.x refused for this request', fallback: x => `unauthorized! ${x} is refused for this request` }\n}\n\n/**\n * 권한 거절 오류. `code: 'FORBIDDEN'` + `requires` + `because` 를 `extensions` 에 담는다.\n *\n * `because` 는 화면이 「권한 요청」을 띄울지 정하는 칸이다 — 역할로 풀리는 것은 `role` 뿐이다.\n * 부르는 쪽이 이유를 모르면(옛 호출) 이름이 있으면 `role`, 없으면 `ownership` 이다 — 지금까지의 뜻 그대로.\n *\n * @param context koa 컨텍스트(요청의 언어를 아는 `t` 를 가진다). 없어도 동작한다.\n */\n/** Which kind of operation was refused - a screen offers an access request only for a change the person made. */\nexport type RefusedOperation = 'query' | 'mutation' | 'subscription'\n\nexport function privilegeRejection(\n context: any,\n category?: string,\n privilege?: string,\n because?: RefusalCause,\n operation?: RefusedOperation\n): GraphQLError {\n const named = !!(category && privilege)\n const requires: PrivilegeRequirement = named ? { category, privilege } : 'ownership'\n const cause: RefusalCause = because ?? (named ? 'role' : 'ownership')\n\n let message: string\n if (named) {\n const x = `${category}:${privilege}`\n const { key, fallback } = NAMED[cause] ?? NAMED.unknown\n message = localized(context, key, { x }, fallback(x))\n } else if (cause === 'address') {\n message = localized(\n context,\n 'error.ownership not allowed from this address',\n {},\n 'unauthorized! ownership does not count from this address'\n )\n } else {\n message = localized(context, 'error.ownership required', {}, 'unauthorized! domain or system ownership required')\n }\n\n return new GraphQLError(message, { extensions: { code: 'FORBIDDEN', requires, because: cause, ...(operation ? { operation } : {}) } })\n}\n"]}
@@ -8,6 +8,7 @@ const typeorm_1 = require("typeorm");
8
8
  const env_1 = require("@things-factory/env");
9
9
  const shell_1 = require("@things-factory/shell");
10
10
  const delete_user_js_1 = require("../../controllers/delete-user.js");
11
+ const assignable_roles_js_1 = require("../../utils/assignable-roles.js");
11
12
  const get_domain_users_js_1 = require("../../utils/get-domain-users.js");
12
13
  const domain_ownership_js_1 = require("../../utils/domain-ownership.js");
13
14
  const role_js_1 = require("../role/role.js");
@@ -342,10 +343,9 @@ let UserMutation = class UserMutation {
342
343
  if (user.domains.map((d) => d.id).indexOf(domain.id) < 0) {
343
344
  throw new Error(`User is not belongs to current domain`);
344
345
  }
345
- const availableRoleIds = availableRoles.map((r) => r.id);
346
- user.roles = user.roles.filter((r) => availableRoleIds.indexOf(r.id) < 0);
347
- user.roles = user.roles.concat(selectedRoles);
348
- return await userRepository.save(user);
346
+ /* Found again in this domain's scope - the ids are the client's (utils/assignable-roles.ts, ADR-0103 decision 2). */
347
+ const { user: saved } = await (0, assignable_roles_js_1.assignRoles)(user, { add: selectedRoles.map(role => role.id), remove: availableRoles.map(role => role.id) }, domain, tx);
348
+ return saved;
349
349
  }
350
350
  };
351
351
  exports.UserMutation = UserMutation;
@@ -485,6 +485,7 @@ tslib_1.__decorate([
485
485
  tslib_1.__decorate([
486
486
  (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)'),
487
487
  (0, type_graphql_1.Directive)('@transaction'),
488
+ (0, type_graphql_1.UseMiddleware)(shell_1.SpeakRefusals),
488
489
  (0, type_graphql_1.Mutation)(returns => user_js_1.User, {
489
490
  description: "Updates the roles assigned to a user. Use this mutation to change a user's permissions within the domain."
490
491
  }),