@things-factory/auth-base 10.1.34 → 10.1.38

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 (37) hide show
  1. package/dist-client/auth.d.ts +8 -0
  2. package/dist-client/auth.js +26 -0
  3. package/dist-client/auth.js.map +1 -1
  4. package/dist-client/domain-required.d.ts +54 -0
  5. package/dist-client/domain-required.js +91 -0
  6. package/dist-client/domain-required.js.map +1 -0
  7. package/dist-client/tsconfig.tsbuildinfo +1 -1
  8. package/dist-server/controllers/invitation.d.ts +26 -0
  9. package/dist-server/controllers/invitation.js +12 -1
  10. package/dist-server/controllers/invitation.js.map +1 -1
  11. package/dist-server/service/app-binding/app-binding-query.d.ts +17 -0
  12. package/dist-server/service/app-binding/app-binding-query.js +32 -2
  13. package/dist-server/service/app-binding/app-binding-query.js.map +1 -1
  14. package/dist-server/service/index.d.ts +7 -2
  15. package/dist-server/service/index.js +3 -0
  16. package/dist-server/service/index.js.map +1 -1
  17. package/dist-server/service/open-fields.d.ts +9 -0
  18. package/dist-server/service/open-fields.js +24 -0
  19. package/dist-server/service/open-fields.js.map +1 -0
  20. package/dist-server/service/privilege/open-field-rule.d.ts +93 -0
  21. package/dist-server/service/privilege/open-field-rule.js +81 -0
  22. package/dist-server/service/privilege/open-field-rule.js.map +1 -0
  23. package/dist-server/service/privilege/privilege-directive.d.ts +9 -1
  24. package/dist-server/service/privilege/privilege-directive.js +177 -95
  25. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  26. package/dist-server/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +4 -4
  28. package/tests/app-binding-db.test.ts +48 -0
  29. package/tests/domain-required.test.ts +127 -0
  30. package/tests/invitation-route.test.ts +97 -0
  31. package/tests/open-doors.test.ts +13 -21
  32. package/tests/open-fields-enforcement.test.ts +215 -0
  33. package/translations/en.json +0 -1
  34. package/translations/ja.json +5 -6
  35. package/translations/ko.json +6 -7
  36. package/translations/ms.json +0 -1
  37. package/translations/zh.json +5 -6
@@ -6,9 +6,11 @@ const tslib_1 = require("tslib");
6
6
  const graphql_1 = require("graphql");
7
7
  const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
8
8
  const utils_1 = require("@graphql-tools/utils");
9
+ const env_1 = require("@things-factory/env");
9
10
  const check_permission_js_1 = require("../../utils/check-permission.js");
10
11
  const privilege_axis_js_1 = require("./privilege-axis.js");
11
12
  const privilege_rejection_js_1 = require("./privilege-rejection.js");
13
+ const open_field_rule_js_1 = require("./open-field-rule.js");
12
14
  process['PRIVILEGES'] = {};
13
15
  /*
14
16
  * ── What each field asks for, by `Type.field` ─────────────────────────────────
@@ -63,110 +65,190 @@ exports.privilegeDirectiveTypeDefs = (0, graphql_tag_1.default) `
63
65
  superUserGranted: Boolean
64
66
  ) on FIELD_DEFINITION
65
67
  `;
66
- const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
67
- [utils_1.MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {
68
- const privilegeDirective = (0, utils_1.getDirective)(schema, fieldConfig, DIRECTIVE)?.[0];
69
- /* A root field with no directive is recorded as open, so "open" and "no such field" differ. */
70
- if (!privilegeDirective && isRootType(schema, typeName)) {
71
- process['PRIVILEGE_OF_FIELD'][`${typeName}.${fieldName}`] = null;
72
- }
73
- if (privilegeDirective) {
74
- const { resolve = graphql_1.defaultFieldResolver, args } = fieldConfig;
75
- if (!args) {
76
- throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`);
77
- }
78
- const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective;
79
- const where = `${typeName}.${fieldName}`;
80
- /*
81
- * A declaration has to name something.
82
- *
83
- * `category` and `privilege` are one thing, not two: together they name a row in the
84
- * privilege table, and either alone names no row, so `User.hasPrivilege` is never
85
- * reached and the field falls back on ownership flags it did not set.
86
- */
87
- if (!category !== !privilege) {
88
- throw new Error(`@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +
89
- `The pair names the privilege to look for; one half names none.`);
90
- }
91
- /*
92
- * `@privilege()` with nothing in it reads like a gate and guards nothing. Until
93
- * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered
94
- * false from a safe address and refused everyone, true from an unsafe one and let
95
- * everyone through. Both are false now, so the shape is merely useless rather than
96
- * dangerous — and a field that is open should say so by carrying no directive, not by
97
- * carrying an empty one.
98
- *
99
- * It is also the shape someone reaches for wanting "being signed in is enough". That
100
- * is a real thing to want and this directive does not express it; the answer settled
101
- * on 2026-09-15 is a category for the published side of a module, `<module>-published`.
102
- *
103
- * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled
104
- * for a resolver that declares nothing. A decorator that declares nothing is that
105
- * same resolver wearing one.
106
- */
107
- if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {
108
- throw new Error(`@privilege on ${where} declares nothing. ` +
109
- `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +
110
- `ownership gate. A field open to everyone carries no directive at all.`);
111
- }
112
- /*
113
- * The axis has to be one this installation declared.
114
- *
115
- * Without this, `privilege: "mutaiton"` registered a pair nobody would ever be granted
116
- * — a gate shut forever, with no error to read. The list is not the framework's: see
117
- * `privilege-axis.ts` for why, and for the fact that it must be declared at module
118
- * import time rather than on a bootstrap hook.
119
- */
120
- if (privilege && !(0, privilege_axis_js_1.isDeclaredAxis)(privilege)) {
121
- throw new Error(`@privilege on ${where} uses axis "${privilege}", which this installation does not declare. ` +
122
- `Declared axes: ${[...(0, privilege_axis_js_1.declaredAxes)()].sort().join(', ')}. ` +
123
- `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +
124
- `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`);
125
- }
126
- if (category && privilege) {
127
- process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
68
+ /**
69
+ * The server calls this with the merged schema only. `modules` and `mode` are for tests, which
70
+ * have no module walk of their own (`orderedModuleNames` is empty under jest) — see `open-fields.ts`.
71
+ */
72
+ const privilegeDirectiveResolver = (schema, options = {}) => {
73
+ const coverage = (0, open_field_rule_js_1.coverageOf)(options.modules ?? (0, open_field_rule_js_1.modulesFromLoader)());
74
+ const mode = options.mode ?? (0, open_field_rule_js_1.enforcementMode)();
75
+ const unlisted = [];
76
+ const mapped = (0, utils_1.mapSchema)(schema, {
77
+ [utils_1.MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {
78
+ const privilegeDirective = (0, utils_1.getDirective)(schema, fieldConfig, DIRECTIVE)?.[0];
79
+ /* A root field with no directive is recorded as open, so "open" and "no such field" differ. */
80
+ if (!privilegeDirective && isRootType(schema, typeName)) {
81
+ const where = `${typeName}.${fieldName}`;
82
+ process['PRIVILEGE_OF_FIELD'][where] = null;
83
+ /*
84
+ * ── The field that arrived without saying anything ───────────────────
85
+ * Its module opted in (declared `openFields`) and names it in neither list. That is how
86
+ * `deleteAppBinding` went in, and the whole point of §8 step 1 is that it cannot happen
87
+ * quietly any more. A field of a module that has not opted in is left exactly as before.
88
+ */
89
+ if ((0, open_field_rule_js_1.judgeUndeclared)(where, coverage) === 'unlisted') {
90
+ if (mode === 'boot') {
91
+ unlisted.push(where);
92
+ }
93
+ else {
94
+ refuseUnlisted(fieldConfig, where);
95
+ }
96
+ }
128
97
  }
129
- /* After every refusal above, so a declaration that names nothing never lands here. */
130
- process['PRIVILEGE_OF_FIELD'][where] = {
131
- category: category || undefined,
132
- privilege: privilege || undefined,
133
- owner: !!domainOwnerGranted,
134
- super: !!superUserGranted
135
- };
136
- // 필드의 기존 description 가져오기
137
- const existingDescription = fieldConfig.description || '';
138
- // 권한 정보를 포함한 새로운 description 생성
139
- const privilegeDescription = `\n\n🔒 Requires privilege: ${category}:${privilege}` +
140
- (domainOwnerGranted ? ', Domain ownership' : '') +
141
- (superUserGranted ? ', System ownership' : '');
142
- // 기존 description과 결합
143
- fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim();
144
- fieldConfig.resolve = async function (source, args, context, info) {
145
- const { domain, user, unsafeIP, prohibitedPrivileges } = context.state;
146
- const permitted = await (0, check_permission_js_1.checkPermission)({
147
- category,
148
- privilege,
149
- owner: domainOwnerGranted,
150
- super: superUserGranted
151
- }, user, domain, unsafeIP, prohibitedPrivileges);
152
- if (permitted) {
153
- return await resolve.call(this, source, args, context, info);
98
+ if (privilegeDirective) {
99
+ const { resolve = graphql_1.defaultFieldResolver, args } = fieldConfig;
100
+ if (!args) {
101
+ throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`);
154
102
  }
103
+ const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective;
104
+ const where = `${typeName}.${fieldName}`;
155
105
  /*
156
- * 안 열렸으면 **거기서 끝이다.**
106
+ * A declaration has to name something.
107
+ *
108
+ * `category` and `privilege` are one thing, not two: together they name a row in the
109
+ * privilege table, and either alone names no row, so `User.hasPrivilege` is never
110
+ * reached and the field falls back on ownership flags it did not set.
111
+ */
112
+ if (!category !== !privilege) {
113
+ throw new Error(`@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +
114
+ `The pair names the privilege to look for; one half names none.`);
115
+ }
116
+ /*
117
+ * `@privilege()` with nothing in it reads like a gate and guards nothing. Until
118
+ * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered
119
+ * false from a safe address and refused everyone, true from an unsafe one and let
120
+ * everyone through. Both are false now, so the shape is merely useless rather than
121
+ * dangerous — and a field that is open should say so by carrying no directive, not by
122
+ * carrying an empty one.
123
+ *
124
+ * It is also the shape someone reaches for wanting "being signed in is enough". That
125
+ * is a real thing to want and this directive does not express it; the answer settled
126
+ * on 2026-09-15 is a category for the published side of a module, `<module>-published`.
157
127
  *
158
- * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지
159
- * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.
128
+ * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled
129
+ * for a resolver that declares nothing. A decorator that declares nothing is that
130
+ * same resolver wearing one.
131
+ */
132
+ if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {
133
+ throw new Error(`@privilege on ${where} declares nothing. ` +
134
+ `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +
135
+ `ownership gate. A field open to everyone carries no directive at all.`);
136
+ }
137
+ /*
138
+ * The axis has to be one this installation declared.
160
139
  *
161
- * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.
140
+ * Without this, `privilege: "mutaiton"` registered a pair nobody would ever be granted
141
+ * — a gate shut forever, with no error to read. The list is not the framework's: see
142
+ * `privilege-axis.ts` for why, and for the fact that it must be declared at module
143
+ * import time rather than on a bootstrap hook.
144
+ */
145
+ if (privilege && !(0, privilege_axis_js_1.isDeclaredAxis)(privilege)) {
146
+ throw new Error(`@privilege on ${where} uses axis "${privilege}", which this installation does not declare. ` +
147
+ `Declared axes: ${[...(0, privilege_axis_js_1.declaredAxes)()].sort().join(', ')}. ` +
148
+ `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +
149
+ `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`);
150
+ }
151
+ if (category && privilege) {
152
+ process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
153
+ }
154
+ /* After every refusal above, so a declaration that names nothing never lands here. */
155
+ process['PRIVILEGE_OF_FIELD'][where] = {
156
+ category: category || undefined,
157
+ privilege: privilege || undefined,
158
+ owner: !!domainOwnerGranted,
159
+ super: !!superUserGranted
160
+ };
161
+ // 필드의 기존 description 가져오기
162
+ const existingDescription = fieldConfig.description || '';
163
+ // 권한 정보를 포함한 새로운 description 생성
164
+ const privilegeDescription = `\n\n🔒 Requires privilege: ${category}:${privilege}` +
165
+ (domainOwnerGranted ? ', Domain ownership' : '') +
166
+ (superUserGranted ? ', System ownership' : '');
167
+ // 기존 description과 결합
168
+ fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim();
169
+ fieldConfig.resolve = async function (source, args, context, info) {
170
+ const { domain, user, unsafeIP, prohibitedPrivileges } = context.state;
171
+ const permitted = await (0, check_permission_js_1.checkPermission)({
172
+ category,
173
+ privilege,
174
+ owner: domainOwnerGranted,
175
+ super: superUserGranted
176
+ }, user, domain, unsafeIP, prohibitedPrivileges);
177
+ if (permitted) {
178
+ return await resolve.call(this, source, args, context, info);
179
+ }
180
+ /*
181
+ * 안 열렸으면 **거기서 끝이다.**
182
+ *
183
+ * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지
184
+ * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.
185
+ *
186
+ * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.
187
+ */
188
+ throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege);
189
+ };
190
+ /*
191
+ * ── A subscription is opened by `subscribe`, not `resolve` ────────────
192
+ * Wrapping only `resolve` left a declared subscription connectable by anyone: the stream
193
+ * opened and every event then failed. It leaked nothing, and it refused nothing either.
194
+ * The same question is asked where the stream starts.
162
195
  */
163
- throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege);
164
- };
165
- return fieldConfig;
196
+ if (fieldConfig.subscribe) {
197
+ const subscribe = fieldConfig.subscribe;
198
+ fieldConfig.subscribe = async function (source, args, context, info) {
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);
203
+ }
204
+ return await subscribe.call(this, source, args, context, info);
205
+ };
206
+ }
207
+ return fieldConfig;
208
+ }
166
209
  }
210
+ });
211
+ /*
212
+ * Every unlisted field at once, rather than the first one. The person who meets this has usually
213
+ * added several resolvers together, and fixing them one boot at a time is how a list gets
214
+ * abandoned half done.
215
+ */
216
+ if (unlisted.length) {
217
+ throw new Error(`${unlisted.length} field(s) declare no @privilege and are not listed as open by their module:\n` +
218
+ unlisted.map(where => ` ${where}`).join('\n') +
219
+ `\n\nFor each, either give it @privilege(category: …, privilege: …), or name it in the module's ` +
220
+ `schema.openFields — under byDesign with the reason it is open, or under notYetDeclared if it ` +
221
+ `is waiting to be closed. See docs/design/auth-rebuild.md §8.`);
167
222
  }
168
- });
223
+ return mapped;
224
+ };
169
225
  exports.privilegeDirectiveResolver = privilegeDirectiveResolver;
226
+ /**
227
+ * Production's answer to an unlisted field: that field refuses, and says so in the log once.
228
+ *
229
+ * The server stays up. What cannot happen is the field answering — it arrived without saying who
230
+ * may call it, and the answer to that is nobody until somebody says.
231
+ */
232
+ const reported = new Set();
233
+ function refuseUnlisted(fieldConfig, where) {
234
+ const refuse = () => {
235
+ if (!reported.has(where)) {
236
+ reported.add(where);
237
+ env_1.logger.error(`${where} declares no @privilege and is not listed in its module's schema.openFields — refusing it.`);
238
+ }
239
+ return new graphql_1.GraphQLError(`${where} is not available: it declares no privilege.`, {
240
+ extensions: { code: 'FORBIDDEN', refusal: { code: 'field-declares-no-privilege', params: { field: where } } }
241
+ });
242
+ };
243
+ fieldConfig.resolve = async () => {
244
+ throw refuse();
245
+ };
246
+ if (fieldConfig.subscribe) {
247
+ fieldConfig.subscribe = async () => {
248
+ throw refuse();
249
+ };
250
+ }
251
+ }
170
252
  function isRootType(schema, typeName) {
171
253
  return [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].some(root => root?.name === typeName);
172
254
  }
@@ -1 +1 @@
1
- {"version":3,"file":"privilege-directive.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-directive.ts"],"names":[],"mappings":";;;AA6CA,4CAIC;;AAjDD,qCAA6D;AAC7D,sEAA6B;AAE7B,gDAA0E;AAG1E,yEAAiE;AACjE,2DAAkE;AAClE,qEAA6D;AAE7D,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;AACM,MAAM,0BAA0B,GAAG,CAAC,MAAqB,EAAE,EAAE,CAClE,IAAA,iBAAS,EAAC,MAAM,EAAE;IAChB,CAAC,kBAAU,CAAC,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QACtE,MAAM,kBAAkB,GAAG,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAE5E,+FAA+F;QAC/F,IAAI,CAAC,kBAAkB,IAAI,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;YACxD,OAAO,CAAC,oBAAoB,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,IAAI,CAAA;QAClE,CAAC;QAED,IAAI,kBAAkB,EAAE,CAAC;YACvB,MAAM,EAAE,OAAO,GAAG,8BAAoB,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;YAE5D,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,8EAA8E,SAAS,GAAG,CAAC,CAAA;YAC7G,CAAC;YAED,MAAM,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,kBAAkB,CAAA;YACxF,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;YAExC;;;;;;eAMG;YACH,IAAI,CAAC,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,8BAA8B,IAAI;oBAC5G,gEAAgE,CACnE,CAAA;YACH,CAAC;YAED;;;;;;;;;;;;;;;eAeG;YACH,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,IAAI,CAAC,kBAAkB,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACxE,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,qBAAqB;oBACzC,mGAAmG;oBACnG,uEAAuE,CAC1E,CAAA;YACH,CAAC;YAED;;;;;;;eAOG;YACH,IAAI,SAAS,IAAI,CAAC,IAAA,kCAAc,EAAC,SAAS,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,eAAe,SAAS,+CAA+C;oBAC3F,kBAAkB,CAAC,GAAG,IAAA,gCAAY,GAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;oBAC3D,2CAA2C,SAAS,uCAAuC;oBAC3F,oFAAoF,CACvF,CAAA;YACH,CAAC;YAED,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;gBAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;YAC3E,CAAC;YAED,sFAAsF;YACtF,OAAO,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG;gBACrC,QAAQ,EAAE,QAAQ,IAAI,SAAS;gBAC/B,SAAS,EAAE,SAAS,IAAI,SAAS;gBACjC,KAAK,EAAE,CAAC,CAAC,kBAAkB;gBAC3B,KAAK,EAAE,CAAC,CAAC,gBAAgB;aACN,CAAA;YAErB,0BAA0B;YAC1B,MAAM,mBAAmB,GAAG,WAAW,CAAC,WAAW,IAAI,EAAE,CAAA;YAEzD,gCAAgC;YAChC,MAAM,oBAAoB,GACxB,8BAA8B,QAAQ,IAAI,SAAS,EAAE;gBACrD,CAAC,kBAAkB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChD,CAAC,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;YAEhD,qBAAqB;YACrB,WAAW,CAAC,WAAW,GAAG,GAAG,mBAAmB,IAAI,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAA;YAEjF,WAAW,CAAC,OAAO,GAAG,KAAK,WAAW,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;gBAC/D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;gBAEtE,MAAM,SAAS,GAAG,MAAM,IAAA,qCAAe,EACrC;oBACE,QAAQ;oBACR,SAAS;oBACT,KAAK,EAAE,kBAAkB;oBACzB,KAAK,EAAE,gBAAgB;iBACxB,EACD,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;gBAED,IAAI,SAAS,EAAE,CAAC;oBACd,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;gBAC9D,CAAC;gBAED;;;;;;;mBAOG;gBACH,MAAM,IAAA,2CAAkB,EAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAA;YACxD,CAAC,CAAA;YAED,OAAO,WAAW,CAAA;QACpB,CAAC;IACH,CAAC;CACF,CAAC,CAAA;AArIS,QAAA,0BAA0B,8BAqInC;AAEJ,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, 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'\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`\nexport const privilegeDirectiveResolver = (schema: GraphQLSchema) =>\n 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 process['PRIVILEGE_OF_FIELD'][`${typeName}.${fieldName}`] = null\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 return fieldConfig\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":"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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"]}