@learncard/network-brain-service 3.18.0 → 3.19.0
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/.env.example +4 -0
- package/CHANGELOG.md +80 -0
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
- package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
- package/dist/cache/atomic-counter.test.d.ts +2 -0
- package/dist/cache/atomic-counter.test.d.ts.map +1 -0
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/config/environment.d.ts +46 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
- package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential.helpers.d.ts +2 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
- package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
- package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
- package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox.helpers.d.ts +9 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
- package/dist/helpers/shim.d.ts +1 -1
- package/dist/helpers/shim.d.ts.map +1 -1
- package/dist/inbox-queue-local.d.ts +2 -0
- package/dist/inbox-queue-local.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AuthGrant.d.ts.map +1 -1
- package/dist/models/CredentialActivity.d.ts +1 -0
- package/dist/models/CredentialActivity.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -2
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/InboxCredential.d.ts +1 -0
- package/dist/models/InboxCredential.d.ts.map +1 -1
- package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/routes/act-as.test.d.ts +2 -0
- package/dist/routes/act-as.test.d.ts.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +10 -0
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +494 -0
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +953 -11
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +42 -126
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
- package/dist/services/registry/registry.factory.d.ts.map +1 -1
- package/dist/types/activity.d.ts +4 -0
- package/dist/types/activity.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +10 -1
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/inbox-batch.d.ts +38 -0
- package/dist/types/inbox-batch.d.ts.map +1 -0
- package/inbox-e2e-setup.ts +53 -1
- package/inbox-elasticmq.conf +10 -0
- package/lambda.ts +61 -16
- package/package.json +18 -16
- package/scripts/setup-inbox-queues.ts +60 -0
- package/serverless-local.yml +2 -0
- package/serverless.yml +75 -0
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/credential-activity/create.ts +1 -0
- package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
- package/src/accesslayer/credential-refresh/index.ts +1 -0
- package/src/accesslayer/credential-refresh/read.ts +8 -2
- package/src/accesslayer/credential-refresh/update.ts +4 -0
- package/src/accesslayer/inbox-batch/store.ts +469 -0
- package/src/accesslayer/inbox-credential/create.ts +51 -19
- package/src/accesslayer/inbox-credential/refresh.ts +221 -0
- package/src/accesslayer/inbox-credential/update.ts +1 -1
- package/src/cache/atomic-counter.test-helpers.ts +27 -0
- package/src/cache/atomic-counter.test.ts +6 -0
- package/src/cache/index.ts +25 -13
- package/src/config/environment.ts +26 -0
- package/src/credential-refresh.ts +5 -1
- package/src/docker-entry.ts +8 -0
- package/src/helpers/activity.helpers.ts +4 -0
- package/src/helpers/app-counter.helpers.ts +1 -1
- package/src/helpers/contact-method.helpers.test.ts +43 -0
- package/src/helpers/contact-method.helpers.ts +3 -4
- package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
- package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
- package/src/helpers/credential-refresh-email.helpers.ts +201 -0
- package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
- package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
- package/src/helpers/credential-refresh.helpers.ts +375 -99
- package/src/helpers/credential.helpers.ts +4 -1
- package/src/helpers/finalize-inbox.helpers.ts +29 -14
- package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
- package/src/helpers/inbox-batch-http.helpers.ts +150 -0
- package/src/helpers/inbox-batch-status.helpers.ts +11 -0
- package/src/helpers/inbox-batch.helpers.test.ts +668 -0
- package/src/helpers/inbox-batch.helpers.ts +374 -0
- package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
- package/src/helpers/inbox-queue.helpers.test.ts +253 -0
- package/src/helpers/inbox-queue.helpers.ts +372 -0
- package/src/helpers/inbox-refresh.helpers.ts +432 -0
- package/src/helpers/inbox.helpers.ts +268 -32
- package/src/helpers/notifications.helpers.ts +4 -2
- package/src/helpers/rateLimit.helpers.ts +2 -5
- package/src/helpers/refresh-send-intent.helpers.ts +273 -0
- package/src/helpers/shim.ts +32 -31
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/CredentialActivity.ts +3 -5
- package/src/models/CredentialRefresh.ts +4 -2
- package/src/models/InboxCredential.ts +2 -0
- package/src/models/credential-refresh-constraints.test.ts +8 -3
- package/src/models/credential-refresh-constraints.ts +5 -0
- package/src/openapi.ts +8 -0
- package/src/routes/act-as.test.ts +239 -0
- package/src/routes/app-store.ts +2 -2
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +914 -92
- package/src/routes/credential-refreshes.test.ts +8 -2
- package/src/routes/credential-refreshes.ts +4 -1
- package/src/routes/credentials.ts +2 -1
- package/src/routes/inbox.ts +95 -49
- package/src/routes/index.ts +82 -7
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +43 -24
- package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
- package/src/services/delivery/adapters/postmark.adapter.ts +12 -0
- package/src/services/registry/registry.factory.ts +2 -1
- package/src/types/activity.ts +2 -0
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/inbox-batch.ts +46 -0
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +2 -0
- package/test/helpers/inbox-e2e-environment.ts +6 -0
- package/test/inbox-batch.spec.ts +1338 -0
- package/test/inbox-refresh.spec.ts +534 -0
- package/test/openapi.spec.ts +41 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- package/vitest.config.ts +9 -0
- package/vitest.inbox-e2e.config.ts +5 -1
- package/vitest.integration.config.ts +2 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { resolveTenantFromRequest } from '@learncard/email-templates';
|
|
3
|
+
import { ACT_AS_HEADER, AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
|
|
4
|
+
import type { ProfileType } from 'types/profile';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
getProfileByDid: vi.fn(),
|
|
8
|
+
getProfileByProfileId: vi.fn(),
|
|
9
|
+
getProfilesThatManageAProfile: vi.fn(),
|
|
10
|
+
grant: vi.fn(),
|
|
11
|
+
verifyPresentation: vi.fn(),
|
|
12
|
+
isAuthGrantAssociatedWithProfile: vi.fn(),
|
|
13
|
+
getAuthGrantById: vi.fn(),
|
|
14
|
+
updateAuthGrant: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@accesslayer/profile/read', () => ({
|
|
18
|
+
getProfileByDid: mocks.getProfileByDid,
|
|
19
|
+
getProfileByProfileId: mocks.getProfileByProfileId,
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('@accesslayer/profile/relationships/read', () => ({
|
|
22
|
+
getProfilesThatManageAProfile: mocks.getProfilesThatManageAProfile,
|
|
23
|
+
}));
|
|
24
|
+
vi.mock('@accesslayer/auth-grant/read', () => ({
|
|
25
|
+
isAuthGrantChallengeValidForDID: mocks.grant,
|
|
26
|
+
isAuthGrantAssociatedWithProfile: mocks.isAuthGrantAssociatedWithProfile,
|
|
27
|
+
getAuthGrantById: mocks.getAuthGrantById,
|
|
28
|
+
}));
|
|
29
|
+
vi.mock('@accesslayer/auth-grant/update', () => ({ updateAuthGrant: mocks.updateAuthGrant }));
|
|
30
|
+
vi.mock('@helpers/learnCard.helpers', () => ({
|
|
31
|
+
getEmptyLearnCard: async () => ({ invoke: { verifyPresentation: mocks.verifyPresentation } }),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import { createContext, didRoute, profileRoute, t, type Context } from './index';
|
|
35
|
+
import { profilesRouter } from './profiles';
|
|
36
|
+
import { authGrantsRouter } from './auth-grants';
|
|
37
|
+
|
|
38
|
+
const domain = 'network.example.com';
|
|
39
|
+
const manager: ProfileType = {
|
|
40
|
+
profileId: 'manager',
|
|
41
|
+
did: 'did:key:manager',
|
|
42
|
+
displayName: 'Manager',
|
|
43
|
+
shortBio: '',
|
|
44
|
+
bio: '',
|
|
45
|
+
};
|
|
46
|
+
const target: ProfileType = {
|
|
47
|
+
profileId: 'the-id',
|
|
48
|
+
did: `did:web:${domain}:users:the-id`,
|
|
49
|
+
displayName: 'District',
|
|
50
|
+
shortBio: '',
|
|
51
|
+
bio: '',
|
|
52
|
+
};
|
|
53
|
+
const inspectionRouter = t.router({
|
|
54
|
+
context: didRoute.query(({ ctx }) => ctx.user),
|
|
55
|
+
writeInbox: profileRoute.meta({ requiredScope: 'inbox:write' }).query(() => true),
|
|
56
|
+
writeBoost: profileRoute.meta({ requiredScope: 'boosts:write' }).query(() => true),
|
|
57
|
+
});
|
|
58
|
+
const context = (overrides: Partial<Context> = {}): Context => ({
|
|
59
|
+
domain,
|
|
60
|
+
tenant: resolveTenantFromRequest({}),
|
|
61
|
+
actAs: target.profileId,
|
|
62
|
+
user: { did: manager.did, isChallengeValid: true, scope: '*:*' },
|
|
63
|
+
...overrides,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.resetAllMocks();
|
|
68
|
+
mocks.getProfileByDid.mockResolvedValue(manager);
|
|
69
|
+
mocks.getProfileByProfileId.mockImplementation(async (id: string) =>
|
|
70
|
+
id === target.profileId ? target : null
|
|
71
|
+
);
|
|
72
|
+
mocks.getProfilesThatManageAProfile.mockResolvedValue([manager]);
|
|
73
|
+
mocks.verifyPresentation.mockResolvedValue({ warnings: [], errors: [], checks: ['JWS'] });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('act as a managed profile', () => {
|
|
77
|
+
it.each([target.profileId, target.did])('accepts seed-auth delegation to %s', async actAs => {
|
|
78
|
+
const ctx = context({ actAs });
|
|
79
|
+
await expect(profilesRouter.createCaller(ctx).getProfile()).resolves.toMatchObject(target);
|
|
80
|
+
await expect(inspectionRouter.createCaller(ctx).context()).resolves.toMatchObject({
|
|
81
|
+
did: target.did,
|
|
82
|
+
profile: target,
|
|
83
|
+
onBehalfOf: manager.profileId,
|
|
84
|
+
scope: '*:*',
|
|
85
|
+
});
|
|
86
|
+
expect(ctx.user?.did).toBe(manager.did);
|
|
87
|
+
expect(mocks.getProfilesThatManageAProfile).toHaveBeenCalledWith(target.profileId);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects a profile the caller does not manage', async () => {
|
|
91
|
+
mocks.getProfilesThatManageAProfile.mockResolvedValue([]);
|
|
92
|
+
await expect(inspectionRouter.createCaller(context()).context()).rejects.toMatchObject({
|
|
93
|
+
code: 'FORBIDDEN',
|
|
94
|
+
message: 'You do not manage profile "the-id".',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('rejects an unknown profile', async () => {
|
|
99
|
+
await expect(
|
|
100
|
+
inspectionRouter.createCaller(context({ actAs: 'missing' })).context()
|
|
101
|
+
).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it.each([
|
|
105
|
+
'did:key:other',
|
|
106
|
+
'did:web:elsewhere:users:the-id',
|
|
107
|
+
`did:web:${domain}:manager:the-id`,
|
|
108
|
+
`${target.did}:extra`,
|
|
109
|
+
])('rejects an unsupported DID: %s', async actAs => {
|
|
110
|
+
await expect(
|
|
111
|
+
inspectionRouter.createCaller(context({ actAs })).context()
|
|
112
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it.each([
|
|
116
|
+
[undefined, false],
|
|
117
|
+
['', false],
|
|
118
|
+
['*', true],
|
|
119
|
+
['other-id', false],
|
|
120
|
+
['the-id,other', true],
|
|
121
|
+
[' other, the-id ', true],
|
|
122
|
+
] as const)('enforces token policy %s (allowed: %s)', async (actAs, allowed) => {
|
|
123
|
+
mocks.grant.mockResolvedValue({ isChallengeValid: true, scope: 'inbox:write', actAs });
|
|
124
|
+
const payload = Buffer.from(
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
vp: { holder: manager.did },
|
|
127
|
+
nonce: `${AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX}test`,
|
|
128
|
+
})
|
|
129
|
+
).toString('base64url');
|
|
130
|
+
const ctx = await createContext({
|
|
131
|
+
req: {
|
|
132
|
+
headers: new Map([
|
|
133
|
+
['authorization', `Bearer e30.${payload}.signature`],
|
|
134
|
+
[ACT_AS_HEADER, target.profileId],
|
|
135
|
+
]),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const caller = inspectionRouter.createCaller(ctx);
|
|
139
|
+
if (!allowed) {
|
|
140
|
+
await expect(caller.context()).rejects.toMatchObject({
|
|
141
|
+
code: 'FORBIDDEN',
|
|
142
|
+
message: 'This API token may not act as "the-id". Grant actAs on the token.',
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await expect(caller.context()).resolves.toMatchObject({
|
|
147
|
+
profile: target,
|
|
148
|
+
did: target.did,
|
|
149
|
+
onBehalfOf: manager.profileId,
|
|
150
|
+
scope: 'inbox:write',
|
|
151
|
+
isAuthGrant: true,
|
|
152
|
+
actAsPolicy: actAs,
|
|
153
|
+
});
|
|
154
|
+
await expect(caller.writeInbox()).resolves.toBe(true);
|
|
155
|
+
await expect(caller.writeBoost()).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('leaves the authenticated identity unchanged without a header', async () => {
|
|
159
|
+
const result = await inspectionRouter.createCaller(context({ actAs: undefined })).context();
|
|
160
|
+
expect(result).toMatchObject({ profile: manager, did: manager.did, scope: '*:*' });
|
|
161
|
+
expect(result.onBehalfOf).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it.each([ACT_AS_HEADER, ACT_AS_HEADER.toLowerCase(), ACT_AS_HEADER.toUpperCase()])(
|
|
165
|
+
'reads header %s without resolving a profile in createContext',
|
|
166
|
+
async name => {
|
|
167
|
+
const ctx = await createContext({ req: { headers: new Map([[name, target.did]]) } });
|
|
168
|
+
expect(ctx.actAs).toBe(target.did);
|
|
169
|
+
expect(mocks.getProfileByProfileId).not.toHaveBeenCalled();
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('act-as ordering', () => {
|
|
175
|
+
it('does not resolve the target when the challenge is not verified', async () => {
|
|
176
|
+
const ctx = context({ user: { did: manager.did, isChallengeValid: false, scope: '*:*' } });
|
|
177
|
+
await expect(inspectionRouter.createCaller(ctx).context()).resolves.toMatchObject({
|
|
178
|
+
did: manager.did,
|
|
179
|
+
});
|
|
180
|
+
expect(mocks.getProfileByProfileId).not.toHaveBeenCalled();
|
|
181
|
+
expect(mocks.getProfilesThatManageAProfile).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('treats a blank header as no delegation', async () => {
|
|
185
|
+
const ctx = context({ actAs: ' ' });
|
|
186
|
+
await expect(inspectionRouter.createCaller(ctx).context()).resolves.toMatchObject({
|
|
187
|
+
did: manager.did,
|
|
188
|
+
});
|
|
189
|
+
expect(mocks.getProfileByProfileId).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('auth grant actAs is immutable', () => {
|
|
194
|
+
const grant = {
|
|
195
|
+
id: 'grant-1',
|
|
196
|
+
name: 'svc',
|
|
197
|
+
challenge: `${AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX}test`,
|
|
198
|
+
status: 'active',
|
|
199
|
+
scope: 'inbox:write',
|
|
200
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
201
|
+
actAs: '*',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
mocks.isAuthGrantAssociatedWithProfile.mockResolvedValue(true);
|
|
206
|
+
mocks.getAuthGrantById.mockResolvedValue(grant);
|
|
207
|
+
mocks.updateAuthGrant.mockResolvedValue(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it.each([{ actAs: 'the-id' }, { actAs: '*' }, { scope: '*:*' }, { status: 'revoked' }])(
|
|
211
|
+
'rejects %j with BAD_REQUEST instead of silently dropping it',
|
|
212
|
+
async updates => {
|
|
213
|
+
const caller = authGrantsRouter.createCaller(context({ actAs: undefined }));
|
|
214
|
+
await expect(
|
|
215
|
+
caller.updateAuthGrant({ id: grant.id, updates: updates as never })
|
|
216
|
+
).rejects.toMatchObject({
|
|
217
|
+
code: 'BAD_REQUEST',
|
|
218
|
+
message: expect.stringContaining('Cannot update id, scope, actAs'),
|
|
219
|
+
});
|
|
220
|
+
expect(mocks.updateAuthGrant).not.toHaveBeenCalled();
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
it('rejects unknown keys', async () => {
|
|
225
|
+
const caller = authGrantsRouter.createCaller(context({ actAs: undefined }));
|
|
226
|
+
await expect(
|
|
227
|
+
caller.updateAuthGrant({ id: grant.id, updates: { bogus: true } as never })
|
|
228
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
229
|
+
expect(mocks.updateAuthGrant).not.toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('still allows mutable fields', async () => {
|
|
233
|
+
const caller = authGrantsRouter.createCaller(context({ actAs: undefined }));
|
|
234
|
+
await expect(
|
|
235
|
+
caller.updateAuthGrant({ id: grant.id, updates: { description: 'renamed' } })
|
|
236
|
+
).resolves.toBe(true);
|
|
237
|
+
expect(mocks.updateAuthGrant).toHaveBeenCalledWith(grant, { description: 'renamed' });
|
|
238
|
+
});
|
|
239
|
+
});
|
package/src/routes/app-store.ts
CHANGED
|
@@ -1671,7 +1671,7 @@ const handleSendNotificationEvent = async (
|
|
|
1671
1671
|
// tRPC does not support HTTP 429 natively, so we cast to BAD_REQUEST
|
|
1672
1672
|
// while keeping the semantic code in the message for clients.
|
|
1673
1673
|
throw new TRPCError({
|
|
1674
|
-
code: 'TOO_MANY_REQUESTS'
|
|
1674
|
+
code: 'TOO_MANY_REQUESTS',
|
|
1675
1675
|
message: 'Rate limit exceeded: max 10 notifications per user per app per hour',
|
|
1676
1676
|
});
|
|
1677
1677
|
}
|
|
@@ -2718,7 +2718,7 @@ export const appStoreRouter = t.router({
|
|
|
2718
2718
|
// tRPC does not support HTTP 429 natively, so we cast to BAD_REQUEST
|
|
2719
2719
|
// while keeping the semantic code in the message for clients.
|
|
2720
2720
|
throw new TRPCError({
|
|
2721
|
-
code: 'TOO_MANY_REQUESTS'
|
|
2721
|
+
code: 'TOO_MANY_REQUESTS',
|
|
2722
2722
|
message: 'Rate limit exceeded: max 60 notifications per app per hour',
|
|
2723
2723
|
});
|
|
2724
2724
|
}
|
|
@@ -139,14 +139,9 @@ export const authGrantsRouter = t.router({
|
|
|
139
139
|
.input(
|
|
140
140
|
z.object({
|
|
141
141
|
id: z.string(),
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
status: true,
|
|
146
|
-
createdAt: true,
|
|
147
|
-
expiresAt: true,
|
|
148
|
-
challenge: true,
|
|
149
|
-
}),
|
|
142
|
+
// Immutable fields stay in the schema so the guard below can reject them
|
|
143
|
+
// explicitly; omitting them would strip the key and silently no-op instead.
|
|
144
|
+
updates: AuthGrantValidator.partial().strict(),
|
|
150
145
|
})
|
|
151
146
|
)
|
|
152
147
|
.output(z.boolean())
|
|
@@ -158,13 +153,20 @@ export const authGrantsRouter = t.router({
|
|
|
158
153
|
});
|
|
159
154
|
}
|
|
160
155
|
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
const invalidUpdates = [
|
|
157
|
+
'id',
|
|
158
|
+
'scope',
|
|
159
|
+
'actAs',
|
|
160
|
+
'status',
|
|
161
|
+
'createdAt',
|
|
162
|
+
'expiresAt',
|
|
163
|
+
'challenge',
|
|
164
|
+
];
|
|
163
165
|
if (invalidUpdates.some(key => input.updates.hasOwnProperty(key))) {
|
|
164
166
|
throw new TRPCError({
|
|
165
167
|
code: 'BAD_REQUEST',
|
|
166
168
|
message:
|
|
167
|
-
'Cannot update id, scope, status, createdAt, expiresAt, or challenge of an AuthGrant',
|
|
169
|
+
'Cannot update id, scope, actAs, status, createdAt, expiresAt, or challenge of an AuthGrant',
|
|
168
170
|
});
|
|
169
171
|
}
|
|
170
172
|
|