@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,189 +0,0 @@
|
|
|
1
|
-
import { config } from '@things-factory/env'
|
|
2
|
-
|
|
3
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
4
|
-
import { loadCompiled } from './compiled'
|
|
5
|
-
|
|
6
|
-
const jwt = require('jsonwebtoken')
|
|
7
|
-
|
|
8
|
-
const { User, UserStatus } = loadCompiled('service/user/user')
|
|
9
|
-
const { ApplianceMutation } = loadCompiled('service/appliance/appliance-mutation')
|
|
10
|
-
|
|
11
|
-
const mutation = new ApplianceMutation()
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* What an installation that configures nothing sees (ADR-0085 decisions 1 and 2).
|
|
15
|
-
*
|
|
16
|
-
* ── Why this file exists ────────────────────────────────────────────────────
|
|
17
|
-
* An appliance token lives a year, and the ones in the field were issued by the version running
|
|
18
|
-
* now. The whole design rests on one claim: **taking this release changes nothing until the
|
|
19
|
-
* installation asks for it.** That claim is easy to state and easy to break — a door that
|
|
20
|
-
* forgets its switch, a serial that starts filling in on its own — so it is held here rather
|
|
21
|
-
* than in prose.
|
|
22
|
-
*
|
|
23
|
-
* These cases run with no config set at all, which is what `config.get(key, false)` sees in a
|
|
24
|
-
* deployment whose config file was never touched.
|
|
25
|
-
*
|
|
26
|
-
* `appliance-revoke-db.test.ts` holds the other side: what the features do once they are on.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
let domain: any
|
|
30
|
-
let actor: any
|
|
31
|
-
|
|
32
|
-
function context(d: any = domain) {
|
|
33
|
-
return {
|
|
34
|
-
state: { domain: d, user: actor, tx: undefined },
|
|
35
|
-
throw: (code: number, message: string) => {
|
|
36
|
-
const error: any = new Error(message)
|
|
37
|
-
error.status = code
|
|
38
|
-
throw error
|
|
39
|
-
}
|
|
40
|
-
} as any
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function shadowUserOf(applianceId: string) {
|
|
44
|
-
return await getRepository(User).findOneBy({ reference: applianceId, userType: 'appliance' })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
beforeAll(async () => {
|
|
48
|
-
await openAuthDatabase()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
afterAll(async () => {
|
|
52
|
-
await closeAuthDatabase()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
beforeEach(async () => {
|
|
56
|
-
await resetAuthDatabase()
|
|
57
|
-
|
|
58
|
-
domain = await getRepository(Domain).save({ name: 'acme', subdomain: 'acme' })
|
|
59
|
-
actor = await getRepository(User).save({
|
|
60
|
-
username: 'operator@acme.z',
|
|
61
|
-
email: 'operator@acme.z',
|
|
62
|
-
name: 'operator',
|
|
63
|
-
status: UserStatus.ACTIVATED,
|
|
64
|
-
domains: [domain]
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('an installation that configures neither feature', () => {
|
|
69
|
-
it('reads both switches as off', () => {
|
|
70
|
-
/*
|
|
71
|
-
* The mechanism the rest of this file depends on. `config.get` answers the default when the
|
|
72
|
-
* key is absent, so "never configured" and "off" are one thing.
|
|
73
|
-
*/
|
|
74
|
-
expect(config.get('credential/encryptAtRest', false)).toBeFalsy()
|
|
75
|
-
expect(config.get('credential/revocable', false)).toBeFalsy()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('has no revocation door', async () => {
|
|
79
|
-
/*
|
|
80
|
-
* ⚠ The case the switch exists for. `revokeApplianceCredential` shipped before this design
|
|
81
|
-
* and went out without a switch; it belongs behind one now, and an installation that never
|
|
82
|
-
* asked for it must not find it.
|
|
83
|
-
*/
|
|
84
|
-
const appliance = await mutation.createAppliance(
|
|
85
|
-
{ name: 'gate-a', serialNo: 'SN-A', brand: 'Acme', model: 'X1' },
|
|
86
|
-
context()
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
await expect(mutation.revokeApplianceCredential(appliance.id, context())).rejects.toThrow(/not enabled/)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('leaves the appliance untouched when the door refuses', async () => {
|
|
93
|
-
/* A refusal that half-applied would be worse than no door at all. */
|
|
94
|
-
const appliance = await mutation.createAppliance(
|
|
95
|
-
{ name: 'gate-b', serialNo: 'SN-B', brand: 'Acme', model: 'X1' },
|
|
96
|
-
context()
|
|
97
|
-
)
|
|
98
|
-
const issued = await mutation.generateApplianceSecret(appliance.id, context())
|
|
99
|
-
|
|
100
|
-
await expect(mutation.revokeApplianceCredential(issued.id, context())).rejects.toThrow()
|
|
101
|
-
|
|
102
|
-
const appuser = await shadowUserOf(issued.id)
|
|
103
|
-
|
|
104
|
-
expect(appuser!.status).toEqual(UserStatus.ACTIVATED)
|
|
105
|
-
expect(appuser!.password).toEqual(issued.accessToken)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('still issues a working credential', async () => {
|
|
109
|
-
/*
|
|
110
|
-
* Registering and issuing are not behind either switch — they are what the product already
|
|
111
|
-
* does. Only revocation and encryption are new.
|
|
112
|
-
*/
|
|
113
|
-
const appliance = await mutation.createAppliance(
|
|
114
|
-
{ name: 'gate-c', serialNo: 'SN-C', brand: 'Acme', model: 'X1' },
|
|
115
|
-
context()
|
|
116
|
-
)
|
|
117
|
-
const issued = await mutation.generateApplianceSecret(appliance.id, context())
|
|
118
|
-
|
|
119
|
-
expect(issued.accessToken).toBeTruthy()
|
|
120
|
-
await expect(User.checkAuth(jwt.decode(issued.accessToken))).resolves.toBeTruthy()
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('stores the credential as it always did', async () => {
|
|
124
|
-
/*
|
|
125
|
-
* ⚠ Encryption is a write-side switch. With it off the value goes in as it always has, so a
|
|
126
|
-
* deployment that takes this release and configures nothing keeps rows in the same shape.
|
|
127
|
-
*/
|
|
128
|
-
const appliance = await mutation.createAppliance(
|
|
129
|
-
{ name: 'gate-d', serialNo: 'SN-D', brand: 'Acme', model: 'X1' },
|
|
130
|
-
context()
|
|
131
|
-
)
|
|
132
|
-
const issued = await mutation.generateApplianceSecret(appliance.id, context())
|
|
133
|
-
|
|
134
|
-
const appuser = await shadowUserOf(issued.id)
|
|
135
|
-
|
|
136
|
-
expect(appuser!.password).toEqual(issued.accessToken)
|
|
137
|
-
expect(appuser!.password.split('.')).toHaveLength(3)
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
describe('a token issued before this release', () => {
|
|
142
|
-
/*
|
|
143
|
-
* Such a token carries no credential serial, because the column did not exist when it was
|
|
144
|
-
* signed. Those are the tokens already in the field, and they are the ones that must not be
|
|
145
|
-
* cut off by taking this release.
|
|
146
|
-
*/
|
|
147
|
-
|
|
148
|
-
it('carries no serial, and is still accepted', async () => {
|
|
149
|
-
const appliance = await mutation.createAppliance(
|
|
150
|
-
{ name: 'gate-e', serialNo: 'SN-E', brand: 'Acme', model: 'X1' },
|
|
151
|
-
context()
|
|
152
|
-
)
|
|
153
|
-
const issued = await mutation.generateApplianceSecret(appliance.id, context())
|
|
154
|
-
|
|
155
|
-
/* Put the subject back the way this release found it: issued, but with no serial recorded. */
|
|
156
|
-
const appuser = await shadowUserOf(issued.id)
|
|
157
|
-
await getRepository(User).save({ ...appuser, credentialSerial: null })
|
|
158
|
-
|
|
159
|
-
const legacyToken = { ...jwt.decode(issued.accessToken) }
|
|
160
|
-
delete legacyToken.cs
|
|
161
|
-
|
|
162
|
-
await expect(User.checkAuth(legacyToken)).resolves.toBeTruthy()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('is accepted even beside a subject that has moved on', async () => {
|
|
166
|
-
/*
|
|
167
|
-
* Two appliances in one installation, one converted and one not. The check has to be per
|
|
168
|
-
* subject — a single flag for the whole installation would cut off everything at once.
|
|
169
|
-
*/
|
|
170
|
-
const a = await mutation.generateApplianceSecret(
|
|
171
|
-
(await mutation.createAppliance({ name: 'gate-f', serialNo: 'SN-F', brand: 'Acme', model: 'X1' }, context())).id,
|
|
172
|
-
context()
|
|
173
|
-
)
|
|
174
|
-
const b = await mutation.generateApplianceSecret(
|
|
175
|
-
(await mutation.createAppliance({ name: 'gate-g', serialNo: 'SN-G', brand: 'Acme', model: 'X1' }, context())).id,
|
|
176
|
-
context()
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
const legacy = await shadowUserOf(a.id)
|
|
180
|
-
await getRepository(User).save({ ...legacy, credentialSerial: null })
|
|
181
|
-
|
|
182
|
-
const legacyToken = { ...jwt.decode(a.accessToken) }
|
|
183
|
-
delete legacyToken.cs
|
|
184
|
-
|
|
185
|
-
await expect(User.checkAuth(legacyToken)).resolves.toBeTruthy()
|
|
186
|
-
await expect(User.checkAuth(jwt.decode(b.accessToken))).resolves.toBeTruthy()
|
|
187
|
-
expect((await shadowUserOf(b.id))!.credentialSerial).toEqual(1)
|
|
188
|
-
})
|
|
189
|
-
})
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { credentialSerialMatches } from '../server/utils/credential-serial-rule'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The one line in `checkAuth` that can refuse a token whose signature is good.
|
|
5
|
-
*
|
|
6
|
-
* Two things have to stay true at once, and they pull in opposite directions:
|
|
7
|
-
*
|
|
8
|
-
* a token issued before this release must pass — or taking the release cuts off the field
|
|
9
|
-
* a token from an earlier generation must not — or revocation does not revoke
|
|
10
|
-
*
|
|
11
|
-
* Both hang on what an absent value means on each side, which is why the cases below are mostly
|
|
12
|
-
* about absence.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
describe('a subject that has never been through the new path', () => {
|
|
16
|
-
it('accepts a token that carries no serial', () => {
|
|
17
|
-
/*
|
|
18
|
-
* ⚠ The tokens already in the field. They were signed before the column existed, and the
|
|
19
|
-
* subjects they name have nothing recorded. If this refused, every appliance would stop on
|
|
20
|
-
* the day the release lands.
|
|
21
|
-
*/
|
|
22
|
-
expect(credentialSerialMatches({ credentialSerial: null }, {})).toBe(true)
|
|
23
|
-
expect(credentialSerialMatches({ credentialSerial: undefined }, {})).toBe(true)
|
|
24
|
-
expect(credentialSerialMatches({}, {})).toBe(true)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('accepts a token that carries one anyway', () => {
|
|
28
|
-
/* Nothing to compare against. The subject decides whether the rule applies, not the token. */
|
|
29
|
-
expect(credentialSerialMatches({ credentialSerial: null }, { cs: 7 })).toBe(true)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('does not fall over on a missing subject or missing claims', () => {
|
|
33
|
-
expect(credentialSerialMatches(undefined as any, {})).toBe(true)
|
|
34
|
-
expect(credentialSerialMatches({ credentialSerial: null }, undefined as any)).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
describe('a subject that has moved on', () => {
|
|
39
|
-
it('accepts the generation it is on', () => {
|
|
40
|
-
expect(credentialSerialMatches({ credentialSerial: 3 }, { cs: 3 })).toBe(true)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('refuses an earlier generation', () => {
|
|
44
|
-
/* ⚠ What revocation rests on. Without this the old token keeps working. */
|
|
45
|
-
expect(credentialSerialMatches({ credentialSerial: 3 }, { cs: 2 })).toBe(false)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('refuses a later one too', () => {
|
|
49
|
-
/* Not a thing we issue, so it is a token from somewhere we did not sign it. */
|
|
50
|
-
expect(credentialSerialMatches({ credentialSerial: 3 }, { cs: 4 })).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('refuses a token with no serial at all', () => {
|
|
54
|
-
/*
|
|
55
|
-
* ⚠ The case the whole design turns on. Once a subject has a serial, the tokens it had
|
|
56
|
-
* before that — the ones with nothing in them — are the revoked ones.
|
|
57
|
-
*/
|
|
58
|
-
expect(credentialSerialMatches({ credentialSerial: 1 }, {})).toBe(false)
|
|
59
|
-
expect(credentialSerialMatches({ credentialSerial: 1 }, { cs: null })).toBe(false)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('refuses the first generation once it has moved past it', () => {
|
|
63
|
-
expect(credentialSerialMatches({ credentialSerial: 2 }, { cs: 1 })).toBe(false)
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('what it compares', () => {
|
|
68
|
-
it('does not take a string for the number', () => {
|
|
69
|
-
/*
|
|
70
|
-
* A claim arrives from a decoded token, so its type is whatever was signed. `'3'` is not the
|
|
71
|
-
* third generation — leaning the other way would let a shaped-but-wrong claim through.
|
|
72
|
-
*/
|
|
73
|
-
expect(credentialSerialMatches({ credentialSerial: 3 }, { cs: '3' as any })).toBe(false)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('treats generation zero as a generation', () => {
|
|
77
|
-
/*
|
|
78
|
-
* We start at 1, so 0 should not appear. But if it ever did, zero is a value and not an
|
|
79
|
-
* absence — reading it as absent would let everything through for that subject.
|
|
80
|
-
*/
|
|
81
|
-
expect(credentialSerialMatches({ credentialSerial: 0 }, { cs: 0 })).toBe(true)
|
|
82
|
-
expect(credentialSerialMatches({ credentialSerial: 0 }, {})).toBe(false)
|
|
83
|
-
})
|
|
84
|
-
})
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A privilege asked in code only - an issued document kind's rule (ADR-0176) - is declared with `declarePrivileges`, so
|
|
3
|
-
* the sync makes its rows and a role task naming it can be applied. 10.1.81 asked `employee-certificate` with
|
|
4
|
-
* `checkPermission` alone; no row was ever made and the template naming it was refused (2026-10-30).
|
|
5
|
-
*/
|
|
6
|
-
import { loadCompiled } from './compiled'
|
|
7
|
-
|
|
8
|
-
const { declarePrivileges } = loadCompiled('service/privilege/privilege-directive')
|
|
9
|
-
const { undeclaredTaskGrants } = loadCompiled('service/role-template/role-task')
|
|
10
|
-
|
|
11
|
-
describe('declarePrivileges', () => {
|
|
12
|
-
it('declares the pairs, so a task naming them is whole', () => {
|
|
13
|
-
declarePrivileges('test kind', [
|
|
14
|
-
['test-certificate', 'query'],
|
|
15
|
-
['test-certificate', 'mutation']
|
|
16
|
-
])
|
|
17
|
-
expect(process['PRIVILEGES']['test-certificate query']).toEqual(['test-certificate', 'query'])
|
|
18
|
-
expect(undeclaredTaskGrants({ id: 'test.issuing', grants: [{ category: 'test-certificate', axes: ['query', 'mutation'] }] })).toEqual([])
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('refuses an axis the installation does not declare, as @privilege does', () => {
|
|
22
|
-
expect(() => declarePrivileges('test kind', [['test-certificate', 'mutaiton']])).toThrow(/axis "mutaiton"/)
|
|
23
|
-
expect(process['PRIVILEGES']['test-certificate mutaiton']).toBeUndefined()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('refuses an empty category', () => {
|
|
27
|
-
expect(() => declarePrivileges('test kind', [['', 'query']])).toThrow(/empty/)
|
|
28
|
-
})
|
|
29
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Who work in a domain may be given to - its activated members and its activated owners (ADR-0138, architect's
|
|
3
|
-
* ruling 2026-10-17). A license's recipient and a sales opportunity's owner ask the same question; the answer is here
|
|
4
|
-
* once. Before it, Office listed members and owners, sales members only, and an owner without a membership row could
|
|
5
|
-
* take a license but not an opportunity.
|
|
6
|
-
*
|
|
7
|
-
* And `getDomainUsers`: it read the users outside the caller's transaction, and looked a domain given without an id
|
|
8
|
-
* up by the id it did not have.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase, ENTITIES } from './db'
|
|
12
|
-
import { loadCompiled } from './compiled'
|
|
13
|
-
|
|
14
|
-
const { DataSource } = require('typeorm')
|
|
15
|
-
const shell = require('@things-factory/shell')
|
|
16
|
-
|
|
17
|
-
const { User } = loadCompiled('service/user/user')
|
|
18
|
-
const { DomainOwner } = loadCompiled('service/domain-owner/domain-owner')
|
|
19
|
-
const { getDomainAssignees, entersDomainAsAssignee, getDomainUsers } = loadCompiled('utils/get-domain-users')
|
|
20
|
-
|
|
21
|
-
let domain: any
|
|
22
|
-
let other: any
|
|
23
|
-
|
|
24
|
-
async function makeUser(name: string, domains: any[], status = 'activated') {
|
|
25
|
-
return await getRepository(User).save({ username: name, email: `${name}@example.com`, name, salt: User.generateSalt(), status, domains })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
beforeAll(async () => {
|
|
29
|
-
await openAuthDatabase()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
afterAll(async () => {
|
|
33
|
-
await closeAuthDatabase()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
beforeEach(async () => {
|
|
37
|
-
await resetAuthDatabase()
|
|
38
|
-
domain = await getRepository(Domain).save({ name: 'hatiolab', subdomain: 'hatiolab' })
|
|
39
|
-
other = await getRepository(Domain).save({ name: 'other', subdomain: 'other' })
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('the people work in a domain may be given to', () => {
|
|
43
|
-
it('are its activated members and its activated owners, by name, each once', async () => {
|
|
44
|
-
await makeUser('bora', [domain])
|
|
45
|
-
await makeUser('chul', [domain], 'inactive')
|
|
46
|
-
const owner = await makeUser('ahn', [other])
|
|
47
|
-
await getRepository(DomainOwner).save({ domain, user: owner })
|
|
48
|
-
const memberOwner = await makeUser('dana', [domain])
|
|
49
|
-
await getRepository(DomainOwner).save({ domain, user: memberOwner })
|
|
50
|
-
const columnOwner = await makeUser('eun', [])
|
|
51
|
-
await getRepository(Domain).update({ id: domain.id }, { owner: columnOwner.id })
|
|
52
|
-
const lockedOwner = await makeUser('fin', [], 'locked')
|
|
53
|
-
await getRepository(DomainOwner).save({ domain, user: lockedOwner })
|
|
54
|
-
await makeUser('gil', [other])
|
|
55
|
-
|
|
56
|
-
const read = await getRepository(Domain).findOneBy({ id: domain.id })
|
|
57
|
-
expect((await getDomainAssignees(read)).map((one: any) => one.name)).toEqual(['ahn', 'bora', 'dana', 'eun'])
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('a member or an owner enters; the status is the caller\'s to refuse', async () => {
|
|
61
|
-
const read = async () => await getRepository(Domain).findOneBy({ id: domain.id })
|
|
62
|
-
const inactive = await makeUser('chul', [domain], 'inactive')
|
|
63
|
-
const owner = await makeUser('ahn', [other])
|
|
64
|
-
await getRepository(DomainOwner).save({ domain, user: owner })
|
|
65
|
-
const stranger = await makeUser('gil', [other])
|
|
66
|
-
|
|
67
|
-
expect(await entersDomainAsAssignee(await read(), inactive)).toBe(true)
|
|
68
|
-
expect(await entersDomainAsAssignee(await read(), owner)).toBe(true)
|
|
69
|
-
expect(await entersDomainAsAssignee(await read(), stranger)).toBe(false)
|
|
70
|
-
expect(await entersDomainAsAssignee(await read(), { id: undefined })).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe('getDomainUsers', () => {
|
|
75
|
-
it('reads through the manager it is given, not past it', async () => {
|
|
76
|
-
/* A second database the framework registry does not know: only the manager reaches it. */
|
|
77
|
-
const elsewhere = await new DataSource({ type: 'sqlite', database: ':memory:', synchronize: true, namingStrategy: new shell.NamingStrategy(), entities: ENTITIES }).initialize()
|
|
78
|
-
try {
|
|
79
|
-
const there = await elsewhere.getRepository(Domain).save({ name: 'there', subdomain: 'there' })
|
|
80
|
-
await elsewhere.getRepository(User).save({ username: 'han', email: 'han@example.com', name: 'han', salt: User.generateSalt(), status: 'activated', domains: [there] })
|
|
81
|
-
|
|
82
|
-
expect((await getDomainUsers(there, elsewhere.manager)).map((one: any) => one.name)).toEqual(['han'])
|
|
83
|
-
expect((await getDomainAssignees(there, elsewhere.manager)).map((one: any) => one.name)).toEqual(['han'])
|
|
84
|
-
} finally {
|
|
85
|
-
await elsewhere.destroy()
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('finds a domain given without its id by the fields it has', async () => {
|
|
90
|
-
await makeUser('bora', [domain])
|
|
91
|
-
expect((await getDomainUsers({ subdomain: 'hatiolab' })).map((one: any) => one.name)).toEqual(['bora'])
|
|
92
|
-
await expect(getDomainUsers({ subdomain: 'nowhere' })).rejects.toThrow(/Failed to find domain/)
|
|
93
|
-
await expect(getDomainUsers({})).rejects.toThrow(/Failed to find domain/)
|
|
94
|
-
})
|
|
95
|
-
})
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What `inherited: Only` binds when the domain has no parent.
|
|
3
|
-
*
|
|
4
|
-
* ── What was here until 2026-09-15 ───────────────────────────────────────────
|
|
5
|
-
* qb.andWhere(`${alias}.domain = :domain`, { domain: domain.parentId || 'Impossible' })
|
|
6
|
-
*
|
|
7
|
-
* "Match nothing" is the right intent — a root domain inherits from nobody — but it was
|
|
8
|
-
* spelled as a **value** bound to a uuid column. sqlite compares it as text and returns zero
|
|
9
|
-
* rows, which is the intended answer arrived at by accident, so it was green through three
|
|
10
|
-
* years of development. postgres parses it and refuses the whole query:
|
|
11
|
-
*
|
|
12
|
-
* invalid input syntax for type uuid: "Impossible" (observed 2026-09-15, path ["roles"])
|
|
13
|
-
*
|
|
14
|
-
* ── Why this test is written against the parameters, not the rows ────────────
|
|
15
|
-
* This harness is sqlite and cannot fail the way the server does; `tests/db.ts` says as much
|
|
16
|
-
* about column dialects. So it reads what TypeORM is about to **send** instead of what comes
|
|
17
|
-
* back. The defect lived in what we build, and what we build is the same on every driver —
|
|
18
|
-
* which makes this the way to reach the whole class our sqlite development cannot see.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase } from './db'
|
|
22
|
-
import { loadCompiled } from './compiled'
|
|
23
|
-
|
|
24
|
-
const { Role } = loadCompiled('service/role/role')
|
|
25
|
-
|
|
26
|
-
const shell = require('@things-factory/shell')
|
|
27
|
-
|
|
28
|
-
const ROOT = { id: '00000000-0000-4000-8000-000000000001', parentId: null }
|
|
29
|
-
const CHILD = { id: '00000000-0000-4000-8000-000000000002', parentId: ROOT.id }
|
|
30
|
-
|
|
31
|
-
beforeAll(async () => {
|
|
32
|
-
await openAuthDatabase()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
afterAll(async () => {
|
|
36
|
-
await closeAuthDatabase()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
function builderFor(domain: { id: string; parentId: string | null }, inherited: string) {
|
|
40
|
-
return shell.getQueryBuilderFromListParams({
|
|
41
|
-
repository: getRepository(Role),
|
|
42
|
-
params: { filters: [], inherited },
|
|
43
|
-
domain,
|
|
44
|
-
searchables: ['name', 'description']
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parametersFor(domain: { id: string; parentId: string | null }, inherited: string): any[] {
|
|
49
|
-
return builderFor(domain, inherited).getQueryAndParameters()[1]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function queryFor(domain: { id: string; parentId: string | null }, inherited: string): string {
|
|
53
|
-
return builderFor(domain, inherited).getQueryAndParameters()[0]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
describe('inherited: Only', () => {
|
|
57
|
-
it('binds the parent id when there is one', () => {
|
|
58
|
-
expect(parametersFor(CHILD, 'Only')).toContain(ROOT.id)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('binds nothing at all when there is no parent', () => {
|
|
62
|
-
/*
|
|
63
|
-
* The sentinel is gone. "A root domain inherits from nobody" is a condition, and it is now
|
|
64
|
-
* written as one — `1 = 0` — rather than as a value that has to survive a uuid parser.
|
|
65
|
-
*/
|
|
66
|
-
expect(parametersFor(ROOT, 'Only')).not.toContain('Impossible')
|
|
67
|
-
|
|
68
|
-
const sql = queryFor(ROOT, 'Only')
|
|
69
|
-
|
|
70
|
-
expect(sql).toContain('1 = 0')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('refuses a value that is not one of the three', () => {
|
|
74
|
-
/*
|
|
75
|
-
* An empty result would be read as "nothing matched". This is a tenant isolation filter;
|
|
76
|
-
* a malfunction here has to be loud, and the enum means no current caller can reach it.
|
|
77
|
-
*/
|
|
78
|
-
expect(() => parametersFor(ROOT, 'Onyl')).toThrow(/unknown inherited value/)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('binds nothing that is not a uuid for the other two scopes', () => {
|
|
82
|
-
/* None and Include are fine, which is why this went unnoticed: two of three scopes work. */
|
|
83
|
-
const uuidish = /^[0-9a-f-]{36}$/i
|
|
84
|
-
|
|
85
|
-
for (const scope of ['None', 'Include']) {
|
|
86
|
-
for (const parameter of parametersFor(ROOT, scope)) {
|
|
87
|
-
if (typeof parameter === 'string') {
|
|
88
|
-
expect(parameter).toMatch(uuidish)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
})
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { decideDomainRedirect, DOMAIN_REDIRECT_WINDOW_MS, domainRequiredTarget } from '../client/domain-required'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Where a screen that came up before the shell settled on a domain is sent.
|
|
5
|
-
*
|
|
6
|
-
* Every query on such a screen answers 403, and "try again" answers 403 again, forever. The shell
|
|
7
|
-
* sends the reader to check-in - the same place the server already sends an HTML request - and
|
|
8
|
-
* never from check-in or sign-in themselves, where it would go round in a circle.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
describe('domainRequiredTarget', () => {
|
|
12
|
-
test('a screen without a domain segment goes to check-in, carrying where it was', () => {
|
|
13
|
-
const target = new URL(domainRequiredTarget('http://localhost:4000/activity-catalog')!)
|
|
14
|
-
|
|
15
|
-
expect(target.pathname).toBe('/auth/checkin')
|
|
16
|
-
expect(target.searchParams.get('redirect_to')).toBe('http://localhost:4000/activity-catalog')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('a screen on a domain the reader may not enter also goes to check-in - the server names why', () => {
|
|
20
|
-
const target = new URL(domainRequiredTarget('http://localhost:4000/domain/other/activity-catalog')!)
|
|
21
|
-
|
|
22
|
-
expect(target.pathname).toBe('/auth/checkin')
|
|
23
|
-
expect(target.searchParams.get('redirect_to')).toBe('http://localhost:4000/domain/other/activity-catalog')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('the query string and hash of the screen survive the round trip', () => {
|
|
27
|
-
const from = 'http://localhost:4000/activity-detail?name=MES%20Disposition%20Approval#line'
|
|
28
|
-
const target = new URL(domainRequiredTarget(from)!)
|
|
29
|
-
|
|
30
|
-
expect(target.searchParams.get('redirect_to')).toBe(from)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('it stays on the same origin', () => {
|
|
34
|
-
expect(new URL(domainRequiredTarget('https://plant.example.com/x')!).origin).toBe('https://plant.example.com')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('check-in itself is never sent to check-in', () => {
|
|
38
|
-
expect(domainRequiredTarget('http://localhost:4000/auth/checkin?redirect_to=%2F')).toBeNull()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('sign-in and the other auth screens are left alone', () => {
|
|
42
|
-
expect(domainRequiredTarget('http://localhost:4000/auth/signin')).toBeNull()
|
|
43
|
-
expect(domainRequiredTarget('http://localhost:4000/auth')).toBeNull()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('a path that only starts with the letters auth is not an auth screen', () => {
|
|
47
|
-
expect(domainRequiredTarget('http://localhost:4000/authority-list')).not.toBeNull()
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('something that is not an address is not guessed at', () => {
|
|
51
|
-
expect(domainRequiredTarget('')).toBeNull()
|
|
52
|
-
expect(domainRequiredTarget('not a url')).toBeNull()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
/*
|
|
56
|
-
* A 403 is not only "no domain". The shell also answers 403 to an address on the site's IP block list
|
|
57
|
-
* (shell/server/middlewares/domain-middleware.ts). That reader has a domain; check-in sends them
|
|
58
|
-
* straight back and the screen gets 403 again. Without this guard the tab goes round forever.
|
|
59
|
-
*/
|
|
60
|
-
describe('after one round trip it stops', () => {
|
|
61
|
-
const now = 1_000_000_000
|
|
62
|
-
const here = 'http://localhost:4000/domain/miratek/activity-catalog'
|
|
63
|
-
|
|
64
|
-
test('sent a moment ago: not sent again, whatever the reason for the 403', () => {
|
|
65
|
-
expect(domainRequiredTarget(here, now - 2_000, now)).toBeNull()
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('it is the time that counts, not the address - the server changed the address on the way back', () => {
|
|
69
|
-
expect(domainRequiredTarget('http://localhost:4000/activity-catalog', now - 2_000, now)).toBeNull()
|
|
70
|
-
expect(domainRequiredTarget(here, now - 2_000, now)).toBeNull()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('once the window has passed, a new 403 is a new case and is sent', () => {
|
|
74
|
-
expect(domainRequiredTarget(here, now - DOMAIN_REDIRECT_WINDOW_MS, now)).not.toBeNull()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('never sent before: sent', () => {
|
|
78
|
-
expect(domainRequiredTarget(here, null, now)).not.toBeNull()
|
|
79
|
-
expect(domainRequiredTarget(here, undefined, now)).not.toBeNull()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('a stamp from the future (a clock that moved back) does not block for ever', () => {
|
|
83
|
-
expect(domainRequiredTarget(here, now + 60_000, now)).not.toBeNull()
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
/*
|
|
88
|
-
* The stamp is the only thing that knows "we just came back". A browser that blocks storage would lose it,
|
|
89
|
-
* and a reader on the IP block list would go round again. So storage that does not work means: do not send.
|
|
90
|
-
*/
|
|
91
|
-
describe('decideDomainRedirect - the stamp must work or nothing is sent', () => {
|
|
92
|
-
const now = 1_000_000_000
|
|
93
|
-
const here = 'http://localhost:4000/activity-catalog'
|
|
94
|
-
const store = (saved: string | null = null) => {
|
|
95
|
-
const box = { value: saved, written: [] as string[] }
|
|
96
|
-
return { box, stamp: { read: () => box.value, write: (v: string) => { box.written.push(v); box.value = v } } }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
test('first time: sent, and the stamp is written before leaving', () => {
|
|
100
|
-
const { box, stamp } = store()
|
|
101
|
-
expect(decideDomainRedirect(here, stamp, now)).not.toBeNull()
|
|
102
|
-
expect(box.written).toEqual([String(now)])
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test('just came back: not sent, and the stamp is left as it was', () => {
|
|
106
|
-
const { box, stamp } = store(String(now - 2_000))
|
|
107
|
-
expect(decideDomainRedirect(here, stamp, now)).toBeNull()
|
|
108
|
-
expect(box.written).toEqual([])
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('storage that cannot be read: not sent', () => {
|
|
112
|
-
const stamp = { read: () => { throw new Error('blocked') }, write: () => {} }
|
|
113
|
-
expect(decideDomainRedirect(here, stamp, now)).toBeNull()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('storage that cannot be written: not sent - a trip we cannot remember could loop', () => {
|
|
117
|
-
const stamp = { read: () => null, write: () => { throw new Error('quota') } }
|
|
118
|
-
expect(decideDomainRedirect(here, stamp, now)).toBeNull()
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test('a stamp that is not a number is treated as none, and the trip goes ahead', () => {
|
|
122
|
-
const { stamp } = store('garbage')
|
|
123
|
-
expect(decideDomainRedirect(here, stamp, now)).not.toBeNull()
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|