@things-factory/auth-base 10.1.278 → 10.1.280
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -7
- package/CHANGELOG.md +0 -55
- package/dist-client/actions/auth.js.map +0 -1
- package/dist-client/apply-domain-theme.js.map +0 -1
- package/dist-client/auth.js.map +0 -1
- package/dist-client/bootstrap.js.map +0 -1
- package/dist-client/directive/privileged.js.map +0 -1
- package/dist-client/domain-required.js.map +0 -1
- package/dist-client/index.js.map +0 -1
- package/dist-client/profiled.js.map +0 -1
- package/dist-client/reducers/auth.js.map +0 -1
- package/dist-client/tsconfig.tsbuildinfo +0 -1
- package/dist-client/verify-webauthn.js.map +0 -1
- package/dist-server/ai-entry/access-assistant.js.map +0 -1
- package/dist-server/ai-entry/access-entry-targets.js.map +0 -1
- package/dist-server/ai-entry/entry-target-shape.js.map +0 -1
- package/dist-server/constants/error-code.js.map +0 -1
- package/dist-server/constants/error-message.js.map +0 -1
- package/dist-server/constants/max-age.js.map +0 -1
- package/dist-server/controllers/access-request.js.map +0 -1
- package/dist-server/controllers/account-mail.js.map +0 -1
- package/dist-server/controllers/auth.js.map +0 -1
- package/dist-server/controllers/change-pwd.js.map +0 -1
- package/dist-server/controllers/confirm-identity.js.map +0 -1
- package/dist-server/controllers/delete-user.js.map +0 -1
- package/dist-server/controllers/passkey.js.map +0 -1
- package/dist-server/controllers/password-setup.js.map +0 -1
- package/dist-server/controllers/profile.js.map +0 -1
- package/dist-server/controllers/reset-password.js.map +0 -1
- package/dist-server/controllers/signin.js.map +0 -1
- package/dist-server/controllers/signup.js.map +0 -1
- package/dist-server/controllers/unlock-user.js.map +0 -1
- package/dist-server/controllers/utils/find-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/make-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/password-rule.js.map +0 -1
- package/dist-server/controllers/utils/save-verification-token.js.map +0 -1
- package/dist-server/controllers/verification.js.map +0 -1
- package/dist-server/errors/auth-error.js.map +0 -1
- package/dist-server/errors/index.js.map +0 -1
- package/dist-server/errors/user-domain-not-match-error.js.map +0 -1
- package/dist-server/index.js.map +0 -1
- package/dist-server/middlewares/audit-scope-middleware.js.map +0 -1
- package/dist-server/middlewares/authenticate-401-middleware.js.map +0 -1
- package/dist-server/middlewares/bypass-signin-middleware.js.map +0 -1
- package/dist-server/middlewares/domain-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/graphql-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/index.js.map +0 -1
- package/dist-server/middlewares/jwt-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/signin-middleware.js.map +0 -1
- package/dist-server/middlewares/verify-recaptcha-middleware.js.map +0 -1
- package/dist-server/migrations/1548206416130-SeedUser.js.map +0 -1
- package/dist-server/migrations/1566805283882-SeedPrivilege.js.map +0 -1
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js.map +0 -1
- package/dist-server/migrations/index.js.map +0 -1
- package/dist-server/role-tasks.js.map +0 -1
- package/dist-server/router/auth-checkin-router.js.map +0 -1
- package/dist-server/router/auth-private-process-router.js.map +0 -1
- package/dist-server/router/auth-public-process-router.js.map +0 -1
- package/dist-server/router/auth-signin-router.js.map +0 -1
- package/dist-server/router/auth-signup-router.js.map +0 -1
- package/dist-server/router/index.js.map +0 -1
- package/dist-server/router/oauth2/index.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-authorize-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-server.js.map +0 -1
- package/dist-server/router/oauth2/passport-oauth2-client-password.js.map +0 -1
- package/dist-server/router/oauth2/passport-refresh-token.js.map +0 -1
- package/dist-server/router/path-base-domain-router.js.map +0 -1
- package/dist-server/router/site-root-router.js.map +0 -1
- package/dist-server/router/webauthn-router.js.map +0 -1
- package/dist-server/routes.js.map +0 -1
- package/dist-server/service/access-request/access-request-mutation.js.map +0 -1
- package/dist-server/service/access-request/access-request-query.js.map +0 -1
- package/dist-server/service/access-request/access-request-types.js.map +0 -1
- package/dist-server/service/access-request/access-request.js.map +0 -1
- package/dist-server/service/access-request/index.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-mutation.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-query.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-types.js.map +0 -1
- package/dist-server/service/app-binding/app-binding.js.map +0 -1
- package/dist-server/service/app-binding/index.js.map +0 -1
- package/dist-server/service/appliance/appliance-mutation.js.map +0 -1
- package/dist-server/service/appliance/appliance-query.js.map +0 -1
- package/dist-server/service/appliance/appliance-types.js.map +0 -1
- package/dist-server/service/appliance/appliance.js.map +0 -1
- package/dist-server/service/appliance/index.js.map +0 -1
- package/dist-server/service/application/application-mutation.js.map +0 -1
- package/dist-server/service/application/application-query.js.map +0 -1
- package/dist-server/service/application/application-types.js.map +0 -1
- package/dist-server/service/application/application.js.map +0 -1
- package/dist-server/service/application/index.js.map +0 -1
- package/dist-server/service/audit/audit-action.js.map +0 -1
- package/dist-server/service/audit/audit-kinds.js.map +0 -1
- package/dist-server/service/audit/audit-query.js.map +0 -1
- package/dist-server/service/audit/audit-recorder.js.map +0 -1
- package/dist-server/service/audit/audit-request.js.map +0 -1
- package/dist-server/service/audit/audit-retention.js.map +0 -1
- package/dist-server/service/audit/audit-subscriber.js.map +0 -1
- package/dist-server/service/audit/audit-target.js.map +0 -1
- package/dist-server/service/audit/audit-types.js.map +0 -1
- package/dist-server/service/audit/audit-work.js.map +0 -1
- package/dist-server/service/audit/index.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-mutation.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-parameter-spec.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-query.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-type.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider.js.map +0 -1
- package/dist-server/service/auth-provider/index.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-mutation.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-types.js.map +0 -1
- package/dist-server/service/domain-generator/index.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-mutation.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-query.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-types.js.map +0 -1
- package/dist-server/service/domain-link/domain-link.js.map +0 -1
- package/dist-server/service/domain-link/index.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-query.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner.js.map +0 -1
- package/dist-server/service/domain-owner/index.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-mutation.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-query.js.map +0 -1
- package/dist-server/service/granted-role/granted-role.js.map +0 -1
- package/dist-server/service/granted-role/index.js.map +0 -1
- package/dist-server/service/identity-confirmation/identity-confirmation.js.map +0 -1
- package/dist-server/service/index.js.map +0 -1
- package/dist-server/service/open-fields.js.map +0 -1
- package/dist-server/service/partner/index.js.map +0 -1
- package/dist-server/service/partner/partner-mutation.js.map +0 -1
- package/dist-server/service/partner/partner-query.js.map +0 -1
- package/dist-server/service/partner/partner-types.js.map +0 -1
- package/dist-server/service/partner/partner.js.map +0 -1
- package/dist-server/service/password-history/index.js.map +0 -1
- package/dist-server/service/password-history/password-history.js.map +0 -1
- package/dist-server/service/privilege/index.js.map +0 -1
- package/dist-server/service/privilege/open-field-rule.js.map +0 -1
- package/dist-server/service/privilege/privilege-axis.js.map +0 -1
- package/dist-server/service/privilege/privilege-deprecation.js.map +0 -1
- package/dist-server/service/privilege/privilege-directive.js.map +0 -1
- package/dist-server/service/privilege/privilege-mutation.js.map +0 -1
- package/dist-server/service/privilege/privilege-query.js.map +0 -1
- package/dist-server/service/privilege/privilege-rejection.js.map +0 -1
- package/dist-server/service/privilege/privilege-sync.js.map +0 -1
- package/dist-server/service/privilege/privilege-types.js.map +0 -1
- package/dist-server/service/privilege/privilege.js.map +0 -1
- package/dist-server/service/role/index.js.map +0 -1
- package/dist-server/service/role/role-mutation.js.map +0 -1
- package/dist-server/service/role/role-privilege-query.js.map +0 -1
- package/dist-server/service/role/role-query.js.map +0 -1
- package/dist-server/service/role/role-types.js.map +0 -1
- package/dist-server/service/role/role.js.map +0 -1
- package/dist-server/service/role-template/index.js.map +0 -1
- package/dist-server/service/role-template/role-task.js.map +0 -1
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +0 -1
- package/dist-server/service/role-template/role-template-mutation.js.map +0 -1
- package/dist-server/service/role-template/role-template-query.js.map +0 -1
- package/dist-server/service/role-template/role-template-types.js.map +0 -1
- package/dist-server/service/role-template/role-template.js.map +0 -1
- package/dist-server/service/user/domain-query.js.map +0 -1
- package/dist-server/service/user/index.js.map +0 -1
- package/dist-server/service/user/user-mutation.js.map +0 -1
- package/dist-server/service/user/user-query.js.map +0 -1
- package/dist-server/service/user/user-types.js.map +0 -1
- package/dist-server/service/user/user.js.map +0 -1
- package/dist-server/service/users-auth-providers/index.js.map +0 -1
- package/dist-server/service/users-auth-providers/users-auth-providers.js.map +0 -1
- package/dist-server/service/verification-token/index.js.map +0 -1
- package/dist-server/service/verification-token/verification-token.js.map +0 -1
- package/dist-server/service/web-auth-credential/index.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-query.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/dist-server/types.js.map +0 -1
- package/dist-server/utils/accepts.js.map +0 -1
- package/dist-server/utils/access-request-review.js.map +0 -1
- package/dist-server/utils/access-token-cookie.js.map +0 -1
- package/dist-server/utils/account-lock-rule.js.map +0 -1
- package/dist-server/utils/assignable-roles.js.map +0 -1
- package/dist-server/utils/build-auth-url.js.map +0 -1
- package/dist-server/utils/can-user-checkin-domain.js.map +0 -1
- package/dist-server/utils/check-permission.js.map +0 -1
- package/dist-server/utils/check-user-belongs-domain.js.map +0 -1
- package/dist-server/utils/check-user-has-role.js.map +0 -1
- package/dist-server/utils/checkin-domain-facts.js.map +0 -1
- package/dist-server/utils/checkin-domain-rule.js.map +0 -1
- package/dist-server/utils/credential-at-rest.js.map +0 -1
- package/dist-server/utils/credential-features.js.map +0 -1
- package/dist-server/utils/credential-serial-rule.js.map +0 -1
- package/dist-server/utils/domain-ownership.js.map +0 -1
- package/dist-server/utils/encrypt-state.js.map +0 -1
- package/dist-server/utils/get-aes-256-key.js.map +0 -1
- package/dist-server/utils/get-domain-users.js.map +0 -1
- package/dist-server/utils/get-secret.js.map +0 -1
- package/dist-server/utils/get-user-domains.js.map +0 -1
- package/dist-server/utils/lock-duration.js.map +0 -1
- package/dist-server/utils/password-reset-rule.js.map +0 -1
- package/dist-server/utils/profile-user.js.map +0 -1
- package/dist-server/utils/refuse-role-name.js.map +0 -1
- package/dist-server/utils/role-name-standing.js.map +0 -1
- package/dist-server/utils/same-origin-redirect.js.map +0 -1
- package/dist-server/utils/verification-token-rule.js.map +0 -1
- package/spec/unit/checkin-domain.spec.ts +0 -307
- package/spec/unit/checkin-privilege.spec.ts +0 -205
- package/spec/unit/domain-owner-entity.spec.ts +0 -179
- package/spec/unit/domain-owner-granted.spec.ts +0 -215
- package/spec/unit/domain-owner-migration.spec.ts +0 -236
- package/spec/unit/domain-owner-mutation.spec.ts +0 -337
- package/spec/unit/domain-ownership.spec.ts +0 -188
- package/tests/access-assistant-db.test.ts +0 -155
- package/tests/access-entry-targets-db.test.ts +0 -176
- package/tests/access-request-db.test.ts +0 -394
- package/tests/access-request-pg.test.ts +0 -83
- package/tests/access-request-review.test.ts +0 -193
- package/tests/account-lock-db.test.ts +0 -216
- package/tests/account-lock-rule.test.ts +0 -141
- package/tests/account-mail.test.ts +0 -69
- package/tests/activate-page-case.test.ts +0 -53
- package/tests/add-user-gates.test.ts +0 -34
- package/tests/app-binding-db.test.ts +0 -210
- package/tests/app-binding-delete-db.test.ts +0 -188
- package/tests/appliance-credential-state-db.test.ts +0 -207
- package/tests/appliance-delete-db.test.ts +0 -119
- package/tests/appliance-revoke-db.test.ts +0 -294
- package/tests/appliance-schema.test.ts +0 -129
- package/tests/application-delete-db.test.ts +0 -190
- package/tests/application-token-payload-db.test.ts +0 -185
- package/tests/assignable-roles-db.test.ts +0 -219
- package/tests/audit-db.test.ts +0 -410
- package/tests/audit-work.test.ts +0 -58
- package/tests/auth-base-role-tasks.test.ts +0 -58
- package/tests/checkin-domain-rule.test.ts +0 -128
- package/tests/checkin-privilege-seam-db.test.ts +0 -158
- package/tests/compiled.ts +0 -41
- package/tests/confirm-identity-db.test.ts +0 -85
- package/tests/credential-at-rest-db.test.ts +0 -246
- package/tests/credential-features-off-db.test.ts +0 -189
- package/tests/credential-serial-rule.test.ts +0 -84
- package/tests/declare-privileges.test.ts +0 -29
- package/tests/domain-assignees-db.test.ts +0 -95
- package/tests/domain-inheritance-sentinel.test.ts +0 -93
- package/tests/domain-required.test.ts +0 -127
- package/tests/has-privilege-db.test.ts +0 -198
- package/tests/identity-confirmation.test.ts +0 -146
- package/tests/list-inheritance-db.test.ts +0 -127
- package/tests/lock-recovery.test.ts +0 -206
- package/tests/mail-request-answers-db.test.ts +0 -142
- package/tests/open-doors.test.ts +0 -121
- package/tests/open-fields-enforcement.test.ts +0 -215
- package/tests/open-fields-framework.test.ts +0 -212
- package/tests/open-redirect-db.test.ts +0 -143
- package/tests/passkey-authenticator.ts +0 -104
- package/tests/passkey-db.test.ts +0 -295
- package/tests/passkey-signin-route-db.test.ts +0 -139
- package/tests/password-hashing.test.ts +0 -131
- package/tests/password-persistence.test.ts +0 -147
- package/tests/password-reset-rule-db.test.ts +0 -159
- package/tests/password-setup-db.test.ts +0 -215
- package/tests/permission-gate.test.ts +0 -228
- package/tests/picture-reducer.test.ts +0 -25
- package/tests/privilege-axis.test.ts +0 -157
- package/tests/privilege-deprecation.test.ts +0 -65
- package/tests/privilege-directive.test.ts +0 -332
- package/tests/privilege-rejection.test.ts +0 -104
- package/tests/privilege-sync-db.test.ts +0 -58
- package/tests/profile-user.test.ts +0 -24
- package/tests/refusal-cause.test.ts +0 -80
- package/tests/refusals-have-korean.test.ts +0 -24
- package/tests/request-holds.test.ts +0 -53
- package/tests/role-inheritance-db.test.ts +0 -286
- package/tests/role-mutation-db.test.ts +0 -293
- package/tests/role-name-lookup-sentinel.test.ts +0 -79
- package/tests/role-name-standing.test.ts +0 -83
- package/tests/role-privileges-db.test.ts +0 -252
- package/tests/role-task.test.ts +0 -168
- package/tests/role-template-gap-db.test.ts +0 -268
- package/tests/role-template-seed-db.test.ts +0 -180
- package/tests/role-template.test.ts +0 -162
- package/tests/role-users-gate.test.ts +0 -22
- package/tests/signin-policy.test.ts +0 -200
- package/tests/signup-duplicate-db.test.ts +0 -68
- package/tests/token-issuance.test.ts +0 -244
- package/tests/user-roles-field-db.test.ts +0 -134
- package/tests/verification-token-db.test.ts +0 -285
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
const mockSentMail: any[] = []
|
|
2
|
-
let mockMailFails = false
|
|
3
|
-
|
|
4
|
-
jest.mock('@things-factory/email-base', () => ({
|
|
5
|
-
...jest.requireActual('@things-factory/email-base'),
|
|
6
|
-
sendEmail: async (mail: any) => {
|
|
7
|
-
if (mockMailFails) throw new Error('smtp down')
|
|
8
|
-
mockSentMail.push(mail)
|
|
9
|
-
}
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
13
|
-
import { loadCompiled } from './compiled'
|
|
14
|
-
import { i18next } from '@things-factory/i18n-base/translator'
|
|
15
|
-
|
|
16
|
-
const { User } = loadCompiled('service/user/user')
|
|
17
|
-
const { authPublicProcessRouter } = loadCompiled('router/auth-public-process-router')
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* What the two "mail me" posts answer - forgot-password and resend-verification-email.
|
|
21
|
-
*
|
|
22
|
-
* Both set nothing when there was no account to mail, or when the mail did not go, so koa answered
|
|
23
|
-
* a bare 404 and the page showed "Not Found" - the same words for "no such address" and "the mail
|
|
24
|
-
* server is down". Now they give one answer whether or not the address has an account (another
|
|
25
|
-
* would tell anyone which addresses are registered), and fail only when a mail was due and did not
|
|
26
|
-
* go.
|
|
27
|
-
*
|
|
28
|
-
* The handler is taken from the router's own table, so the path is the one the page posts to.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
function handler(path: string) {
|
|
32
|
-
const layer = authPublicProcessRouter.stack.find((layer: any) => layer.path === path && layer.methods.includes('POST'))
|
|
33
|
-
|
|
34
|
-
return layer.stack[layer.stack.length - 1]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function post(path: string, body: any) {
|
|
38
|
-
const context: any = {
|
|
39
|
-
t: (key: string) => key,
|
|
40
|
-
i18next,
|
|
41
|
-
lng: 'en-US',
|
|
42
|
-
headers: {},
|
|
43
|
-
header: {},
|
|
44
|
-
request: { protocol: 'https', host: 'acme.example.com', body }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
await handler(path)(context, async () => {})
|
|
48
|
-
|
|
49
|
-
return { status: context.status, body: context.body }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let domain: any
|
|
53
|
-
|
|
54
|
-
async function person(email: string, status: string) {
|
|
55
|
-
const salt = User.generateSalt()
|
|
56
|
-
|
|
57
|
-
return await getRepository(User).save({
|
|
58
|
-
username: email.split('@')[0],
|
|
59
|
-
email,
|
|
60
|
-
name: 'Kim',
|
|
61
|
-
salt,
|
|
62
|
-
password: User.encode('Passw0rd!', salt),
|
|
63
|
-
status,
|
|
64
|
-
domains: [domain]
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
beforeAll(async () => {
|
|
69
|
-
await openAuthDatabase()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
beforeEach(async () => {
|
|
73
|
-
await resetAuthDatabase()
|
|
74
|
-
domain = await getRepository(Domain).save({ name: 'plant-a', subdomain: 'plant-a' })
|
|
75
|
-
mockSentMail.length = 0
|
|
76
|
-
mockMailFails = false
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
afterAll(async () => {
|
|
80
|
-
await closeAuthDatabase()
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('forgot-password', () => {
|
|
84
|
-
const PATH = '/auth/forgot-password'
|
|
85
|
-
|
|
86
|
-
it('mails a registered address', async () => {
|
|
87
|
-
await person('kim@example.com', 'activated')
|
|
88
|
-
|
|
89
|
-
expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 200, body: 'text.password reset email sent' })
|
|
90
|
-
expect(mockSentMail.map(mail => mail.receiver)).toEqual(['kim@example.com'])
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('answers an unknown address the same way, and mails nobody', async () => {
|
|
94
|
-
await person('kim@example.com', 'activated')
|
|
95
|
-
|
|
96
|
-
expect(await post(PATH, { email: 'nobody@example.com' })).toEqual({ status: 200, body: 'text.password reset email sent' })
|
|
97
|
-
expect(mockSentMail).toEqual([])
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('says so when the mail does not go', async () => {
|
|
101
|
-
await person('kim@example.com', 'activated')
|
|
102
|
-
mockMailFails = true
|
|
103
|
-
|
|
104
|
-
expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 503, body: 'error.mail could not be sent' })
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
describe('resend-verification-email', () => {
|
|
109
|
-
const PATH = '/auth/resend-verification-email'
|
|
110
|
-
|
|
111
|
-
it('mails an account waiting for confirmation', async () => {
|
|
112
|
-
await person('kim@example.com', 'inactive')
|
|
113
|
-
|
|
114
|
-
expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 200, body: 'text.verification email sent' })
|
|
115
|
-
expect(mockSentMail.map(mail => mail.receiver)).toEqual(['kim@example.com'])
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it.each([
|
|
119
|
-
['an unknown address', 'nobody@example.com'],
|
|
120
|
-
['an account already confirmed', 'lee@example.com']
|
|
121
|
-
])('answers %s the same way, and mails nobody', async (_, email) => {
|
|
122
|
-
await person('lee@example.com', 'activated')
|
|
123
|
-
|
|
124
|
-
expect(await post(PATH, { email })).toEqual({ status: 200, body: 'text.verification email sent' })
|
|
125
|
-
expect(mockSentMail).toEqual([])
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('mails nobody for a post without an address', async () => {
|
|
129
|
-
/* ⚠ TypeORM drops an undefined condition - this matched the first account and mailed it. */
|
|
130
|
-
await person('kim@example.com', 'inactive')
|
|
131
|
-
|
|
132
|
-
expect(await post(PATH, {})).toEqual({ status: 200, body: 'text.verification email sent' })
|
|
133
|
-
expect(mockSentMail).toEqual([])
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('says so when the mail does not go', async () => {
|
|
137
|
-
await person('kim@example.com', 'inactive')
|
|
138
|
-
mockMailFails = true
|
|
139
|
-
|
|
140
|
-
expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 503, body: 'error.mail could not be sent' })
|
|
141
|
-
})
|
|
142
|
-
})
|
package/tests/open-doors.test.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { buildSchema } from 'type-graphql'
|
|
2
|
-
import { GraphQLSchema } from 'graphql'
|
|
3
|
-
|
|
4
|
-
import { loadCompiled } from './compiled'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Which of auth-base's doors are open, and why each one is allowed to be.
|
|
8
|
-
*
|
|
9
|
-
* ── The thing this catches ──────────────────────────────────────────────────
|
|
10
|
-
* `@privilege` is opt-in. A root field that declares nothing is recorded as open, and that is
|
|
11
|
-
* indistinguishable from a field somebody decided to open — no error, no warning, nothing to
|
|
12
|
-
* grep for. So a door goes in ungated the way `deleteAppBinding` did, and only a reader who
|
|
13
|
-
* happens to look at that one file ever finds out.
|
|
14
|
-
*
|
|
15
|
-
* The list below turns the absence into a statement. Anything open that is not on it fails, and
|
|
16
|
-
* the reason has to be written before it can be added.
|
|
17
|
-
*
|
|
18
|
-
* ── Why this is measured and not read off the source ────────────────────────
|
|
19
|
-
* The schema is built from `service/index.ts`'s own `resolverClasses` — the list the server
|
|
20
|
-
* boots from — and the directive fills `PRIVILEGE_OF_FIELD` while it walks it. A regular
|
|
21
|
-
* expression over the files would answer about the text; this answers about the schema that
|
|
22
|
-
* actually gets served, including every field the walk reaches that a hand-written pattern
|
|
23
|
-
* would not.
|
|
24
|
-
*
|
|
25
|
-
* ── What it does not cover ──────────────────────────────────────────────────
|
|
26
|
-
* Only auth-base. The other packages have their own resolvers and their own open doors, and
|
|
27
|
-
* `docs/design/auth-rebuild.md` §7 is where the policy for those is still undecided.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const { schema: declared } = loadCompiled('service/index')
|
|
31
|
-
const { privilegeDirectiveResolver, privilegeOfField } = loadCompiled('service/privilege/privilege-directive')
|
|
32
|
-
|
|
33
|
-
/*
|
|
34
|
-
* Open on purpose — read from the module's own declaration (`server/service/open-fields.ts`), which is
|
|
35
|
-
* also what the directive enforces against. It used to be a copy kept here, and two copies of one
|
|
36
|
-
* list are two lists.
|
|
37
|
-
*/
|
|
38
|
-
const { openFields: declaration } = loadCompiled('service/open-fields')
|
|
39
|
-
|
|
40
|
-
const OPEN_BY_DECISION: Record<string, Record<string, string>> = { Query: {}, Mutation: {} }
|
|
41
|
-
|
|
42
|
-
for (const [where, reason] of Object.entries(declaration.byDesign as Record<string, string>)) {
|
|
43
|
-
const [typeName, field] = where.split('.')
|
|
44
|
-
OPEN_BY_DECISION[typeName][field] = reason
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let schema: GraphQLSchema
|
|
48
|
-
|
|
49
|
-
beforeAll(async () => {
|
|
50
|
-
schema = await buildSchema({ resolvers: declared.resolverClasses, validate: false })
|
|
51
|
-
|
|
52
|
-
/* No modules: this file measures what is open, not what would be refused. */
|
|
53
|
-
privilegeDirectiveResolver(schema, { modules: [] })
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
function openFields(typeName: 'Query' | 'Mutation'): string[] {
|
|
57
|
-
const type = typeName === 'Query' ? schema.getQueryType() : schema.getMutationType()
|
|
58
|
-
|
|
59
|
-
return Object.keys(type?.getFields() || {}).filter(field => {
|
|
60
|
-
const declaredGate = privilegeOfField(typeName, field)
|
|
61
|
-
|
|
62
|
-
return declaredGate.known && declaredGate['gate'] === null
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
describe('every open door is one somebody chose', () => {
|
|
67
|
-
it.each(['Query', 'Mutation'] as const)('%s', typeName => {
|
|
68
|
-
expect(openFields(typeName).sort()).toEqual(Object.keys(OPEN_BY_DECISION[typeName]).sort())
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe('the doors that were closed', () => {
|
|
73
|
-
/*
|
|
74
|
-
* ⚠ These were all open on 2026-09-20, every one of them reachable by anyone holding a valid
|
|
75
|
-
* token. They are listed by name so that reopening one is a visible act rather than a
|
|
76
|
-
* deletion nobody reads.
|
|
77
|
-
*
|
|
78
|
-
* the five auth provider writes reading the configuration needed a privilege; replacing it
|
|
79
|
-
* did not. An auth provider is how an external identity
|
|
80
|
-
* signs in, so adding one is not an edit to a settings row
|
|
81
|
-
* domainUserRoleRegister makes a domain, its roles and its users. `domainRegister`
|
|
82
|
-
* beside it makes only a domain and is super-user only
|
|
83
|
-
* loginHistories everybody's sign-ins - now the audit's, behind `audit:query` (ADR-0141)
|
|
84
|
-
* inviteUser
|
|
85
|
-
*/
|
|
86
|
-
const CLOSED = {
|
|
87
|
-
Mutation: [
|
|
88
|
-
'createAuthProvider',
|
|
89
|
-
'updateAuthProvider',
|
|
90
|
-
'updateMultipleAuthProvider',
|
|
91
|
-
'deleteAuthProvider',
|
|
92
|
-
'deleteAuthProviders',
|
|
93
|
-
'domainUserRoleRegister',
|
|
94
|
-
'inviteUser'
|
|
95
|
-
],
|
|
96
|
-
Query: ['auditRecords', 'auditPeople', 'auditDoors', 'auditHealth']
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
it.each(Object.entries(CLOSED).flatMap(([t, fields]) => fields.map(f => [t, f])))('%s.%s', (typeName, field) => {
|
|
100
|
-
const declaredGate = privilegeOfField(typeName, field)
|
|
101
|
-
|
|
102
|
-
expect(declaredGate.known).toBe(true)
|
|
103
|
-
expect(declaredGate['gate']).not.toBeNull()
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
describe('what the walk itself has to be true for', () => {
|
|
108
|
-
it('reaches the whole schema, not a handful of fields', () => {
|
|
109
|
-
/*
|
|
110
|
-
* If the resolver list shrank to nothing the two assertions above would pass by having
|
|
111
|
-
* nothing to look at. This is the floor under them.
|
|
112
|
-
*/
|
|
113
|
-
expect(Object.keys(schema.getQueryType()?.getFields() || {}).length).toBeGreaterThan(30)
|
|
114
|
-
expect(Object.keys(schema.getMutationType()?.getFields() || {}).length).toBeGreaterThan(40)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('tells an undeclared field from one the schema does not have', () => {
|
|
118
|
-
/* `known: false` must never read as open — that is how a typo becomes a gate that is not there. */
|
|
119
|
-
expect(privilegeOfField('Query', 'noSuchField')).toEqual({ known: false })
|
|
120
|
-
})
|
|
121
|
-
})
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { buildSchema } from 'type-graphql'
|
|
2
|
-
import { graphql, GraphQLSchema } from 'graphql'
|
|
3
|
-
import { makeExecutableSchema } from '@graphql-tools/schema'
|
|
4
|
-
|
|
5
|
-
import { loadCompiled } from './compiled'
|
|
6
|
-
|
|
7
|
-
const { schema: declared } = loadCompiled('service/index')
|
|
8
|
-
const { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } = loadCompiled('service/privilege/privilege-directive')
|
|
9
|
-
const { coverageOf, judgeUndeclared } = loadCompiled('service/privilege/open-field-rule')
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A field that declares no `@privilege` and that its module does not list — what happens to it
|
|
13
|
-
* (`docs/design/auth-rebuild.md` §8 step 1; question sheet Q1 · Q2 · Q5).
|
|
14
|
-
*
|
|
15
|
-
* ── The four answers ────────────────────────────────────────────────────────
|
|
16
|
-
* byDesign open, and the module says why
|
|
17
|
-
* notYetDeclared open, and on the module's list to close
|
|
18
|
-
* unlisted the module opted in and says nothing: boot refuses in development, the field
|
|
19
|
-
* refuses in production
|
|
20
|
-
* not opted in the module declared no lists at all: exactly as before
|
|
21
|
-
*
|
|
22
|
-
* The last one is deliberate. The four products serve resolvers of their own from another
|
|
23
|
-
* repository, and enforcing on them the day this lands would stop all four from booting.
|
|
24
|
-
*
|
|
25
|
-
* These build auth-base's real schema from the `resolverClasses` the server boots from, and vary
|
|
26
|
-
* only the module's declaration — so what is tested is the directive against real fields, not
|
|
27
|
-
* against a model of them.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const MODULE = 'auth-base'
|
|
31
|
-
const REAL = declared.openFields
|
|
32
|
-
|
|
33
|
-
function modules(openFields: any) {
|
|
34
|
-
return [{ name: MODULE, schema: { resolverClasses: declared.resolverClasses, openFields } }]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** auth-base's declaration with one reason taken out — the shape of a field somebody forgot to list. */
|
|
38
|
-
function without(where: string) {
|
|
39
|
-
const byDesign = { ...REAL.byDesign }
|
|
40
|
-
delete byDesign[where]
|
|
41
|
-
return { ...REAL, byDesign }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function built(): Promise<GraphQLSchema> {
|
|
45
|
-
return await buildSchema({ resolvers: declared.resolverClasses, validate: false })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function refusalCodeOf(promise: Promise<any>): Promise<string> {
|
|
49
|
-
const result = await promise
|
|
50
|
-
return result?.errors?.[0]?.extensions?.refusal?.code || result?.errors?.[0]?.extensions?.code || 'no error'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('who is covered', () => {
|
|
54
|
-
it('attributes each root field to the module whose class declared it', async () => {
|
|
55
|
-
const coverage = coverageOf(modules(REAL))
|
|
56
|
-
|
|
57
|
-
expect(coverage.get('Query.myRoles')).toEqual({ module: MODULE, listed: 'by-design' })
|
|
58
|
-
expect(coverage.get('Mutation.createRole')).toEqual({ module: MODULE, listed: null })
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('covers nothing for a module that declared no lists', () => {
|
|
62
|
-
/* ⚠ The line that keeps the four products booting. */
|
|
63
|
-
expect(coverageOf(modules(undefined)).size).toBe(0)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('does not cover a field of a class the module did not name', () => {
|
|
67
|
-
const coverage = coverageOf([{ name: 'other', schema: { resolverClasses: [], openFields: {} } }])
|
|
68
|
-
|
|
69
|
-
expect(judgeUndeclared('Query.myRoles', coverage)).toEqual('not-opted-in')
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('the verdicts', () => {
|
|
74
|
-
const coverage = () =>
|
|
75
|
-
coverageOf(modules({ byDesign: { 'Query.myRoles': 'mine' }, notYetDeclared: ['Query.passwordRule'] }))
|
|
76
|
-
|
|
77
|
-
it('open by design', () => {
|
|
78
|
-
expect(judgeUndeclared('Query.myRoles', coverage())).toEqual('open-by-design')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('open, not yet declared', () => {
|
|
82
|
-
expect(judgeUndeclared('Query.passwordRule', coverage())).toEqual('open-not-yet-declared')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('unlisted', () => {
|
|
86
|
-
expect(judgeUndeclared('Query.hasPrivilege', coverage())).toEqual('unlisted')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('not opted in', () => {
|
|
90
|
-
expect(judgeUndeclared('Query.somethingElse', coverage())).toEqual('not-opted-in')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
describe('in development — the schema refuses to build', () => {
|
|
95
|
-
it('builds with the declaration auth-base ships', async () => {
|
|
96
|
-
/* Every undeclared field of auth-base is in `byDesign`; nothing is unlisted today. */
|
|
97
|
-
const schema = await built()
|
|
98
|
-
|
|
99
|
-
expect(() => privilegeDirectiveResolver(schema, { modules: modules(REAL), mode: 'boot' })).not.toThrow()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('names the field that is unlisted', async () => {
|
|
103
|
-
/* ⚠ The case the step exists for: a field arrived and nobody said who may call it. */
|
|
104
|
-
const schema = await built()
|
|
105
|
-
|
|
106
|
-
expect(() => privilegeDirectiveResolver(schema, { modules: modules(without('Query.myRoles')), mode: 'boot' })).toThrow(
|
|
107
|
-
/Query\.myRoles/
|
|
108
|
-
)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('names every unlisted field at once, not the first', async () => {
|
|
112
|
-
const schema = await built()
|
|
113
|
-
const declaration = without('Query.myRoles')
|
|
114
|
-
delete declaration.byDesign['Query.passwordRule']
|
|
115
|
-
|
|
116
|
-
let message = ''
|
|
117
|
-
try {
|
|
118
|
-
privilegeDirectiveResolver(schema, { modules: modules(declaration), mode: 'boot' })
|
|
119
|
-
} catch (e: any) {
|
|
120
|
-
message = e.message
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
expect(message).toMatch(/2 field\(s\)/)
|
|
124
|
-
expect(message).toMatch(/Query\.myRoles/)
|
|
125
|
-
expect(message).toMatch(/Query\.passwordRule/)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('accepts the field once it is listed as not yet declared', async () => {
|
|
129
|
-
const schema = await built()
|
|
130
|
-
const declaration = { ...without('Query.myRoles'), notYetDeclared: ['Query.myRoles'] }
|
|
131
|
-
|
|
132
|
-
expect(() => privilegeDirectiveResolver(schema, { modules: modules(declaration), mode: 'boot' })).not.toThrow()
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('leaves a module that declared no lists exactly as it was', async () => {
|
|
136
|
-
const schema = await built()
|
|
137
|
-
|
|
138
|
-
expect(() => privilegeDirectiveResolver(schema, { modules: modules(undefined), mode: 'boot' })).not.toThrow()
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('in production — only that field refuses', () => {
|
|
143
|
-
it('still builds', async () => {
|
|
144
|
-
const schema = await built()
|
|
145
|
-
|
|
146
|
-
expect(() =>
|
|
147
|
-
privilegeDirectiveResolver(schema, { modules: modules(without('Query.passwordRule')), mode: 'request' })
|
|
148
|
-
).not.toThrow()
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('refuses the unlisted field, with a name the screen can read', async () => {
|
|
152
|
-
/*
|
|
153
|
-
* ⚠ Never the other way. An unlisted field in production answers nobody — wrong in the worst case
|
|
154
|
-
* means one field refuses, never one field opens.
|
|
155
|
-
*/
|
|
156
|
-
const schema = privilegeDirectiveResolver(await built(), {
|
|
157
|
-
modules: modules(without('Query.passwordRule')),
|
|
158
|
-
mode: 'request'
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
expect(await refusalCodeOf(graphql({ schema, source: '{ passwordRule { lowerCase } }', contextValue: { state: {} } }))).toEqual(
|
|
162
|
-
'field-declares-no-privilege'
|
|
163
|
-
)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('leaves a listed field answering', async () => {
|
|
167
|
-
const schema = privilegeDirectiveResolver(await built(), {
|
|
168
|
-
modules: modules(without('Query.myRoles')),
|
|
169
|
-
mode: 'request'
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const result = await graphql({ schema, source: '{ passwordRule { lowerCase } }', contextValue: { state: {} } })
|
|
173
|
-
|
|
174
|
-
expect(result.errors?.[0]?.extensions?.refusal).toBeUndefined()
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
describe('a subscription is stopped where it opens', () => {
|
|
179
|
-
/*
|
|
180
|
-
* ⚠ The directive used to wrap `resolve` only. A declared subscription then connected for anyone
|
|
181
|
-
* and failed on every event — it leaked nothing and refused nothing. Built from SDL because
|
|
182
|
-
* auth-base has no subscription of its own; the directive sees the same field configuration.
|
|
183
|
-
*/
|
|
184
|
-
const typeDefs = `
|
|
185
|
-
type Query { ok: Boolean }
|
|
186
|
-
type Subscription {
|
|
187
|
-
guarded: String @privilege(category: "board", privilege: "query")
|
|
188
|
-
}
|
|
189
|
-
`
|
|
190
|
-
|
|
191
|
-
function schemaWith(subscribe: () => any) {
|
|
192
|
-
return privilegeDirectiveResolver(
|
|
193
|
-
makeExecutableSchema({
|
|
194
|
-
typeDefs: [privilegeDirectiveTypeDefs, typeDefs],
|
|
195
|
-
resolvers: { Query: { ok: () => true }, Subscription: { guarded: { subscribe, resolve: (v: any) => v } } }
|
|
196
|
-
}),
|
|
197
|
-
{ modules: [] }
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
it('refuses at subscribe for somebody without the privilege', async () => {
|
|
202
|
-
let opened = false
|
|
203
|
-
const schema = schemaWith(() => {
|
|
204
|
-
opened = true
|
|
205
|
-
return (async function* () {})()
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
const field = schema.getSubscriptionType()!.getFields().guarded as any
|
|
209
|
-
|
|
210
|
-
await expect(
|
|
211
|
-
field.subscribe({}, {}, { state: { user: { id: 'u', domains: [] }, domain: { id: 'd' } } }, {} as any)
|
|
212
|
-
).rejects.toBeTruthy()
|
|
213
|
-
expect(opened).toBe(false)
|
|
214
|
-
})
|
|
215
|
-
})
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { buildSchema, getMetadataStorage } from 'type-graphql'
|
|
2
|
-
import { createPubSub } from '@graphql-yoga/subscription'
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
|
|
6
|
-
import { loadCompiled } from './compiled'
|
|
7
|
-
|
|
8
|
-
const { privilegeDirectiveResolver } = loadCompiled('service/privilege/privilege-directive')
|
|
9
|
-
const companion = loadCompiled('service/index').schema.resolverClasses
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Every things-factory module with resolvers says which of its fields are open (§8 step 1).
|
|
13
|
-
*
|
|
14
|
-
* ── Two modes ───────────────────────────────────────────────────────────────
|
|
15
|
-
* Normally this is the check. For each module that declares `schema.openFields`, the directive
|
|
16
|
-
* must accept it in boot mode — no field is open without being listed — and no name on its lists
|
|
17
|
-
* may be stale (a field that has since been given @privilege, or that no longer exists). A list
|
|
18
|
-
* that keeps names after they are closed stops telling anyone what is left.
|
|
19
|
-
*
|
|
20
|
-
* With `GENERATE_OPEN_FIELDS=<package>[,<package>…]` it writes that module's declaration from
|
|
21
|
-
* what its schema serves today: every undeclared root field goes under `notYetDeclared`, and the
|
|
22
|
-
* module's `server/service/index.ts` gains the one key. Behaviour does not change — everything
|
|
23
|
-
* that was open is listed. Sorting a field into `byDesign` with its reason, or closing it, is the
|
|
24
|
-
* work of step 2 and is done by hand.
|
|
25
|
-
*
|
|
26
|
-
* ── How a module is measured ────────────────────────────────────────────────
|
|
27
|
-
* The way the server builds it: the module's own `resolverClasses`, `buildSchema` with a pub/sub
|
|
28
|
-
* (modules with subscriptions do not build without one), and the privilege directive's record of
|
|
29
|
-
* what each field declares. A module with mutations and no query cannot build alone, so every
|
|
30
|
-
* build carries auth-base's resolvers beside it, and only the module's own fields — by the class
|
|
31
|
-
* that declared them, from type-graphql's metadata — are counted.
|
|
32
|
-
*
|
|
33
|
-
* ⚠ Reads `dist-server`. After editing a module, build it before this means anything.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
const PACKAGES = join(__dirname, '..', '..')
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The modules that have declared their lists. Named rather than counted, the way `screen-words` names
|
|
40
|
-
* the packages that owe words: a module that opts in is added here in the same commit, and the day
|
|
41
|
-
* every module with resolvers is on it, the "not yet" half of this file goes.
|
|
42
|
-
*
|
|
43
|
-
* 2026-09-24: auth-base only. The generator below writes the rest; running it for the framework is
|
|
44
|
-
* the next step of §8 step 1 and was held back from the 10.1.38 cut so it could be verified first.
|
|
45
|
-
*/
|
|
46
|
-
const OPTED_IN = ['auth-base', 'pdf']
|
|
47
|
-
|
|
48
|
-
/** Modules that cannot be measured here, and why. Adding one needs its reason. */
|
|
49
|
-
const CANNOT_MEASURE: Record<string, string> = {
|
|
50
|
-
calendar:
|
|
51
|
-
"its compiled entry throws on load when required on its own (`events` of `Attendee` has no inferable type). None of the four products loads it."
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
type Measured = { name: string; own: string[]; undeclared: string[]; schema: any }
|
|
55
|
-
|
|
56
|
-
function modulesWithResolvers(): string[] {
|
|
57
|
-
return readdirSync(PACKAGES)
|
|
58
|
-
.filter(name => existsSync(join(PACKAGES, name, 'dist-server', 'service', 'index.js')))
|
|
59
|
-
.filter(name => {
|
|
60
|
-
const source = join(PACKAGES, name, 'server', 'service', 'index.ts')
|
|
61
|
-
return existsSync(source) && readFileSync(source, 'utf-8').includes('resolverClasses')
|
|
62
|
-
})
|
|
63
|
-
.sort()
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function measure(name: string): Promise<Measured> {
|
|
67
|
-
const schema = require(join(PACKAGES, name, 'dist-server', 'service', 'index.js')).schema
|
|
68
|
-
const classes = new Set<Function>(schema?.resolverClasses || [])
|
|
69
|
-
|
|
70
|
-
const storage = getMetadataStorage() as any
|
|
71
|
-
const own: string[] = []
|
|
72
|
-
for (const [typeName, bucket] of [
|
|
73
|
-
['Query', 'queries'],
|
|
74
|
-
['Mutation', 'mutations'],
|
|
75
|
-
['Subscription', 'subscriptions']
|
|
76
|
-
]) {
|
|
77
|
-
for (const field of storage[bucket] || []) {
|
|
78
|
-
if (classes.has(field.target)) own.push(`${typeName}.${field.schemaName}`)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
process['PRIVILEGE_OF_FIELD'] = {}
|
|
83
|
-
const built = await buildSchema({
|
|
84
|
-
resolvers: (name === 'auth-base' ? [...classes] : [...companion, ...classes]) as any,
|
|
85
|
-
validate: false,
|
|
86
|
-
pubSub: createPubSub() as any
|
|
87
|
-
})
|
|
88
|
-
privilegeDirectiveResolver(built, { modules: [] })
|
|
89
|
-
|
|
90
|
-
const record = process['PRIVILEGE_OF_FIELD'] as Record<string, unknown>
|
|
91
|
-
const undeclared = own.filter(where => record[where] === null).sort()
|
|
92
|
-
|
|
93
|
-
return { name, own: own.sort(), undeclared, schema }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function declarationSource(name: string, undeclared: string[]): string {
|
|
97
|
-
return `/**
|
|
98
|
-
* ${name}'s root fields that carry no \`@privilege\` (auth-base \`docs/design/auth-rebuild.md\` §8).
|
|
99
|
-
*
|
|
100
|
-
* Written from what this module served on the day it opted in, so nothing that was open stopped
|
|
101
|
-
* working. Every name starts under \`notYetDeclared\`: nobody has yet decided what each one guards.
|
|
102
|
-
* Closing one means giving it \`@privilege(category: …, privilege: …)\` and taking it off this list.
|
|
103
|
-
* A field that is open on purpose moves to \`byDesign\`, with its reason.
|
|
104
|
-
*
|
|
105
|
-
* A new root field that is on neither list, and has no \`@privilege\`, stops the development server
|
|
106
|
-
* and is refused in production — see \`privilege/open-field-rule.ts\` in auth-base.
|
|
107
|
-
*/
|
|
108
|
-
export const openFields = {
|
|
109
|
-
byDesign: {} as Record<string, string>,
|
|
110
|
-
notYetDeclared: [
|
|
111
|
-
${undeclared.map(where => ` '${where}'`).join(',\n')}${undeclared.length ? '\n' : ''} ]
|
|
112
|
-
}
|
|
113
|
-
`
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function optIn(name: string, undeclared: string[]) {
|
|
117
|
-
const dir = join(PACKAGES, name, 'server', 'service')
|
|
118
|
-
writeFileSync(join(dir, 'open-fields.ts'), declarationSource(name, undeclared))
|
|
119
|
-
|
|
120
|
-
const indexPath = join(dir, 'index.ts')
|
|
121
|
-
let index = readFileSync(indexPath, 'utf-8')
|
|
122
|
-
|
|
123
|
-
if (!index.includes("from './open-fields.js'")) {
|
|
124
|
-
/* After the last import line, so the module's own import order is left as it was. */
|
|
125
|
-
const lines = index.split('\n')
|
|
126
|
-
let last = -1
|
|
127
|
-
lines.forEach((line, i) => {
|
|
128
|
-
if (/^import .* from '.*'$/.test(line) || /^} from '.*'$/.test(line)) last = i
|
|
129
|
-
})
|
|
130
|
-
lines.splice(last + 1, 0, "import { openFields } from './open-fields.js'")
|
|
131
|
-
index = lines.join('\n')
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!/export const schema = \{\n\s*openFields,/.test(index)) {
|
|
135
|
-
index = index.replace('export const schema = {', 'export const schema = {\n openFields,')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
writeFileSync(indexPath, index)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const GENERATE = (process.env.GENERATE_OPEN_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
142
|
-
|
|
143
|
-
if (GENERATE.length) {
|
|
144
|
-
it(`writes the declarations for ${GENERATE.join(', ')}`, async () => {
|
|
145
|
-
for (const name of GENERATE) {
|
|
146
|
-
const measured = await measure(name)
|
|
147
|
-
optIn(name, measured.undeclared)
|
|
148
|
-
}
|
|
149
|
-
}, 600000)
|
|
150
|
-
} else {
|
|
151
|
-
const names = modulesWithResolvers().filter(name => !(name in CANNOT_MEASURE))
|
|
152
|
-
|
|
153
|
-
describe('every module with resolvers', () => {
|
|
154
|
-
it('finds the modules it means to check', () => {
|
|
155
|
-
/* A walk that finds nothing passes whatever the modules say. */
|
|
156
|
-
expect(names.length).toBeGreaterThan(50)
|
|
157
|
-
expect(names).toContain('auth-base')
|
|
158
|
-
expect(names).toContain('shell')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('the modules that have opted in are exactly the ones named', async () => {
|
|
162
|
-
/*
|
|
163
|
-
* ⚠ Both directions. A module that declares lists without being named here is caught, and so is
|
|
164
|
-
* one named here that has lost its lists — either way the name list stops saying what is true.
|
|
165
|
-
*/
|
|
166
|
-
const optedIn: string[] = []
|
|
167
|
-
for (const name of names) {
|
|
168
|
-
const schema = require(join(PACKAGES, name, 'dist-server', 'service', 'index.js')).schema
|
|
169
|
-
if (schema?.openFields) optedIn.push(name)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
expect(optedIn.sort()).toEqual([...OPTED_IN].sort())
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it.each(OPTED_IN)('%s says which of its fields are open', async name => {
|
|
176
|
-
const measured = await measure(name)
|
|
177
|
-
|
|
178
|
-
expect(measured.schema?.openFields).toBeTruthy()
|
|
179
|
-
|
|
180
|
-
/* The directive in boot mode is the check the development server runs. */
|
|
181
|
-
process['PRIVILEGE_OF_FIELD'] = {}
|
|
182
|
-
const built = await buildSchema({
|
|
183
|
-
resolvers: (name === 'auth-base'
|
|
184
|
-
? measured.schema.resolverClasses
|
|
185
|
-
: [...companion, ...measured.schema.resolverClasses]) as any,
|
|
186
|
-
validate: false,
|
|
187
|
-
pubSub: createPubSub() as any
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
expect(() =>
|
|
191
|
-
privilegeDirectiveResolver(built, {
|
|
192
|
-
modules: [{ name, schema: measured.schema }],
|
|
193
|
-
mode: 'boot'
|
|
194
|
-
})
|
|
195
|
-
).not.toThrow()
|
|
196
|
-
|
|
197
|
-
/* No stale names: every listed field is one this module serves and has not yet closed. */
|
|
198
|
-
const declaration = measured.schema.openFields
|
|
199
|
-
const listed = [...Object.keys(declaration.byDesign || {}), ...(declaration.notYetDeclared || [])]
|
|
200
|
-
const stale = listed.filter(where => !measured.undeclared.includes(where))
|
|
201
|
-
|
|
202
|
-
expect(stale).toEqual([])
|
|
203
|
-
}, 120000)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
describe('what cannot be measured here', () => {
|
|
207
|
-
it.each(Object.entries(CANNOT_MEASURE))('%s — %s', name => {
|
|
208
|
-
/* Still listed as a module with resolvers — the day it can be measured, this fails and it moves. */
|
|
209
|
-
expect(modulesWithResolvers()).toContain(name)
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
}
|