@selvajs/platform 0.21.0-beta.3 → 1.0.0-beta.5
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/README.md +11 -8
- package/dist/access/index.d.ts +0 -1
- package/dist/access/index.js +0 -1
- package/dist/access/rules.d.ts +7 -14
- package/dist/access/rules.js +22 -26
- package/dist/apiTokens/index.d.ts +1 -2
- package/dist/apiTokens/index.js +1 -2
- package/dist/apiTokens/interface.d.ts +0 -1
- package/dist/apiTokens/interface.js +0 -1
- package/dist/apiTokens/scope.d.ts +0 -1
- package/dist/apiTokens/scope.js +0 -1
- package/dist/apiTokens/types.d.ts +0 -1
- package/dist/apiTokens/types.js +0 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +0 -1
- package/dist/auth/interface.d.ts +4 -23
- package/dist/auth/interface.js +0 -1
- package/dist/auth/types.d.ts +4 -2
- package/dist/auth/types.js +0 -1
- package/dist/bindings/index.d.ts +0 -1
- package/dist/bindings/index.js +0 -1
- package/dist/bindings/interface.d.ts +0 -1
- package/dist/bindings/interface.js +0 -1
- package/dist/computeServer/index.d.ts +1 -7
- package/dist/computeServer/index.js +2 -8
- package/dist/computeServer/interface.d.ts +9 -6
- package/dist/computeServer/interface.js +0 -1
- package/dist/computeServer/public.d.ts +6 -0
- package/dist/computeServer/public.js +2 -0
- package/dist/computeServer/secrets.d.ts +12 -6
- package/dist/computeServer/secrets.js +35 -15
- package/dist/computeServer/types.d.ts +5 -2
- package/dist/computeServer/types.js +4 -1
- package/dist/computeServer/utils.d.ts +17 -6
- package/dist/computeServer/utils.js +31 -21
- package/dist/config.d.ts +1 -2
- package/dist/config.js +0 -1
- package/dist/context.d.ts +11 -1
- package/dist/context.js +51 -2
- package/dist/data/index.d.ts +0 -1
- package/dist/data/index.js +0 -1
- package/dist/data/interface.d.ts +0 -1
- package/dist/data/interface.js +0 -1
- package/dist/definitions/index.d.ts +2 -3
- package/dist/definitions/index.js +2 -3
- package/dist/definitions/interface.d.ts +0 -1
- package/dist/definitions/interface.js +0 -1
- package/dist/definitions/paths.d.ts +0 -1
- package/dist/definitions/paths.js +1 -12
- package/dist/definitions/schemas.d.ts +0 -21
- package/dist/definitions/schemas.js +6 -19
- package/dist/definitions/types.d.ts +3 -4
- package/dist/definitions/types.js +0 -3
- package/dist/errors/interface.d.ts +1 -2
- package/dist/errors/interface.js +0 -1
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +11 -1
- package/dist/events/audit.d.ts +1 -2
- package/dist/events/audit.js +0 -1
- package/dist/events/index.d.ts +0 -1
- package/dist/events/index.js +0 -1
- package/dist/events/interface.d.ts +0 -1
- package/dist/events/interface.js +0 -1
- package/dist/index.d.ts +18 -61
- package/dist/index.js +18 -24
- package/dist/invites/index.d.ts +0 -1
- package/dist/invites/index.js +0 -1
- package/dist/invites/interface.d.ts +5 -6
- package/dist/invites/interface.js +0 -1
- package/dist/invites/types.d.ts +0 -1
- package/dist/invites/types.js +0 -1
- package/dist/logging/interface.d.ts +0 -1
- package/dist/logging/interface.js +0 -1
- package/dist/metrics/interface.d.ts +0 -1
- package/dist/metrics/interface.js +0 -1
- package/dist/notifications/index.d.ts +0 -1
- package/dist/notifications/index.js +0 -1
- package/dist/notifications/interface.d.ts +1 -3
- package/dist/notifications/interface.js +1 -3
- package/dist/notifications/types.d.ts +0 -1
- package/dist/notifications/types.js +0 -1
- package/dist/organizations/index.d.ts +2 -3
- package/dist/organizations/index.js +1 -2
- package/dist/organizations/interface.d.ts +3 -5
- package/dist/organizations/interface.js +0 -1
- package/dist/organizations/paths.d.ts +1 -6
- package/dist/organizations/paths.js +2 -20
- package/dist/organizations/schemas.d.ts +5 -7
- package/dist/organizations/schemas.js +20 -13
- package/dist/organizations/types.d.ts +0 -1
- package/dist/organizations/types.js +0 -1
- package/dist/pagination.d.ts +21 -3
- package/dist/pagination.js +34 -1
- package/dist/permissions/index.d.ts +2 -2
- package/dist/permissions/index.js +2 -2
- package/dist/permissions/interface.d.ts +4 -4
- package/dist/permissions/interface.js +0 -1
- package/dist/permissions/types.d.ts +0 -1
- package/dist/permissions/types.js +0 -1
- package/dist/platformProjects/index.d.ts +1 -3
- package/dist/platformProjects/index.js +1 -2
- package/dist/platformProjects/interface.d.ts +0 -1
- package/dist/platformProjects/interface.js +0 -1
- package/dist/platformProjects/schemas.d.ts +0 -1
- package/dist/platformProjects/schemas.js +0 -1
- package/dist/platformProjects/types.d.ts +3 -2
- package/dist/platformProjects/types.js +0 -1
- package/dist/projects/index.d.ts +0 -1
- package/dist/projects/index.js +0 -1
- package/dist/projects/interface.d.ts +0 -1
- package/dist/projects/interface.js +0 -1
- package/dist/projects/schemas.d.ts +0 -1
- package/dist/projects/schemas.js +0 -1
- package/dist/projects/types.d.ts +0 -1
- package/dist/projects/types.js +0 -1
- package/dist/shareLinks/index.d.ts +0 -1
- package/dist/shareLinks/index.js +0 -1
- package/dist/shareLinks/interface.d.ts +6 -3
- package/dist/shareLinks/interface.js +0 -1
- package/dist/shareLinks/schemas.d.ts +0 -1
- package/dist/shareLinks/schemas.js +4 -2
- package/dist/shareLinks/types.d.ts +0 -1
- package/dist/shareLinks/types.js +0 -1
- package/dist/solveCache/interface.d.ts +2 -3
- package/dist/solveCache/interface.js +0 -1
- package/dist/storage/assetClasses.d.ts +4 -8
- package/dist/storage/assetClasses.js +6 -21
- package/dist/storage/cacheBust.d.ts +0 -1
- package/dist/storage/cacheBust.js +5 -3
- package/dist/storage/image.d.ts +8 -5
- package/dist/storage/image.js +74 -12
- package/dist/storage/index.d.ts +2 -4
- package/dist/storage/index.js +2 -3
- package/dist/storage/interface.d.ts +11 -3
- package/dist/storage/interface.js +0 -1
- package/dist/storage/safeKey.d.ts +7 -0
- package/dist/storage/safeKey.js +12 -0
- package/dist/testing/index.d.ts +5 -2
- package/dist/testing/index.js +3 -2
- package/dist/testing/suites/apiTokenStoreSuite.d.ts +0 -1
- package/dist/testing/suites/apiTokenStoreSuite.js +0 -1
- package/dist/testing/suites/auditQuerySuite.d.ts +18 -0
- package/dist/testing/suites/auditQuerySuite.js +73 -0
- package/dist/testing/suites/authProviderSuite.d.ts +14 -3
- package/dist/testing/suites/authProviderSuite.js +57 -32
- package/dist/testing/suites/computeServerStoreSuite.d.ts +1 -2
- package/dist/testing/suites/computeServerStoreSuite.js +68 -4
- package/dist/testing/suites/definitionStoreSuite.d.ts +2 -1
- package/dist/testing/suites/definitionStoreSuite.js +19 -16
- package/dist/testing/suites/emailLinkAuthSuite.d.ts +2 -5
- package/dist/testing/suites/emailLinkAuthSuite.js +1 -9
- package/dist/testing/suites/eventSinkSuite.d.ts +0 -1
- package/dist/testing/suites/eventSinkSuite.js +9 -1
- package/dist/testing/suites/helpers.d.ts +5 -1
- package/dist/testing/suites/helpers.js +7 -1
- package/dist/testing/suites/inviteStoreSuite.d.ts +0 -1
- package/dist/testing/suites/inviteStoreSuite.js +17 -7
- package/dist/testing/suites/oauthAuthSuite.d.ts +18 -0
- package/dist/testing/suites/oauthAuthSuite.js +26 -0
- package/dist/testing/suites/orgStoreSuite.d.ts +5 -1
- package/dist/testing/suites/orgStoreSuite.js +26 -7
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts +0 -5
- package/dist/testing/suites/platformPermissionStoreSuite.js +12 -63
- package/dist/testing/suites/platformProjectGrantStoreSuite.d.ts +0 -1
- package/dist/testing/suites/platformProjectGrantStoreSuite.js +0 -5
- package/dist/testing/suites/projectStoreSuite.d.ts +5 -1
- package/dist/testing/suites/projectStoreSuite.js +6 -6
- package/dist/testing/suites/shareLinkStoreSuite.d.ts +2 -1
- package/dist/testing/suites/shareLinkStoreSuite.js +22 -25
- package/dist/testing/suites/solveMetricSinkSuite.d.ts +0 -1
- package/dist/testing/suites/solveMetricSinkSuite.js +2 -2
- package/dist/testing/suites/storageProviderSuite.d.ts +0 -1
- package/dist/testing/suites/storageProviderSuite.js +15 -10
- package/dist/testing/suites/userProfileStoreSuite.d.ts +1 -2
- package/dist/testing/suites/userProfileStoreSuite.js +42 -7
- package/dist/userProfile/index.d.ts +2 -2
- package/dist/userProfile/index.js +2 -2
- package/dist/userProfile/interface.d.ts +5 -6
- package/dist/userProfile/interface.js +0 -1
- package/dist/userProfile/types.d.ts +2 -1
- package/dist/userProfile/types.js +2 -1
- package/dist/utils/audit.d.ts +0 -1
- package/dist/utils/audit.js +0 -1
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.js +0 -1
- package/package.json +7 -6
- package/dist/access/index.d.ts.map +0 -1
- package/dist/access/index.js.map +0 -1
- package/dist/access/rules.d.ts.map +0 -1
- package/dist/access/rules.js.map +0 -1
- package/dist/apiTokens/index.d.ts.map +0 -1
- package/dist/apiTokens/index.js.map +0 -1
- package/dist/apiTokens/interface.d.ts.map +0 -1
- package/dist/apiTokens/interface.js.map +0 -1
- package/dist/apiTokens/scope.d.ts.map +0 -1
- package/dist/apiTokens/scope.js.map +0 -1
- package/dist/apiTokens/types.d.ts.map +0 -1
- package/dist/apiTokens/types.js.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/interface.d.ts.map +0 -1
- package/dist/auth/interface.js.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/bindings/index.d.ts.map +0 -1
- package/dist/bindings/index.js.map +0 -1
- package/dist/bindings/interface.d.ts.map +0 -1
- package/dist/bindings/interface.js.map +0 -1
- package/dist/computeServer/index.d.ts.map +0 -1
- package/dist/computeServer/index.js.map +0 -1
- package/dist/computeServer/interface.d.ts.map +0 -1
- package/dist/computeServer/interface.js.map +0 -1
- package/dist/computeServer/secrets.d.ts.map +0 -1
- package/dist/computeServer/secrets.js.map +0 -1
- package/dist/computeServer/types.d.ts.map +0 -1
- package/dist/computeServer/types.js.map +0 -1
- package/dist/computeServer/utils.d.ts.map +0 -1
- package/dist/computeServer/utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/data/index.d.ts.map +0 -1
- package/dist/data/index.js.map +0 -1
- package/dist/data/interface.d.ts.map +0 -1
- package/dist/data/interface.js.map +0 -1
- package/dist/definitions/index.d.ts.map +0 -1
- package/dist/definitions/index.js.map +0 -1
- package/dist/definitions/interface.d.ts.map +0 -1
- package/dist/definitions/interface.js.map +0 -1
- package/dist/definitions/paths.d.ts.map +0 -1
- package/dist/definitions/paths.js.map +0 -1
- package/dist/definitions/schemas.d.ts.map +0 -1
- package/dist/definitions/schemas.js.map +0 -1
- package/dist/definitions/types.d.ts.map +0 -1
- package/dist/definitions/types.js.map +0 -1
- package/dist/errors/interface.d.ts.map +0 -1
- package/dist/errors/interface.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/events/audit.d.ts.map +0 -1
- package/dist/events/audit.js.map +0 -1
- package/dist/events/index.d.ts.map +0 -1
- package/dist/events/index.js.map +0 -1
- package/dist/events/interface.d.ts.map +0 -1
- package/dist/events/interface.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invites/index.d.ts.map +0 -1
- package/dist/invites/index.js.map +0 -1
- package/dist/invites/interface.d.ts.map +0 -1
- package/dist/invites/interface.js.map +0 -1
- package/dist/invites/types.d.ts.map +0 -1
- package/dist/invites/types.js.map +0 -1
- package/dist/logging/interface.d.ts.map +0 -1
- package/dist/logging/interface.js.map +0 -1
- package/dist/metrics/interface.d.ts.map +0 -1
- package/dist/metrics/interface.js.map +0 -1
- package/dist/notifications/index.d.ts.map +0 -1
- package/dist/notifications/index.js.map +0 -1
- package/dist/notifications/interface.d.ts.map +0 -1
- package/dist/notifications/interface.js.map +0 -1
- package/dist/notifications/types.d.ts.map +0 -1
- package/dist/notifications/types.js.map +0 -1
- package/dist/organizations/index.d.ts.map +0 -1
- package/dist/organizations/index.js.map +0 -1
- package/dist/organizations/interface.d.ts.map +0 -1
- package/dist/organizations/interface.js.map +0 -1
- package/dist/organizations/paths.d.ts.map +0 -1
- package/dist/organizations/paths.js.map +0 -1
- package/dist/organizations/schemas.d.ts.map +0 -1
- package/dist/organizations/schemas.js.map +0 -1
- package/dist/organizations/types.d.ts.map +0 -1
- package/dist/organizations/types.js.map +0 -1
- package/dist/pagination.d.ts.map +0 -1
- package/dist/pagination.js.map +0 -1
- package/dist/permissions/index.d.ts.map +0 -1
- package/dist/permissions/index.js.map +0 -1
- package/dist/permissions/interface.d.ts.map +0 -1
- package/dist/permissions/interface.js.map +0 -1
- package/dist/permissions/types.d.ts.map +0 -1
- package/dist/permissions/types.js.map +0 -1
- package/dist/platformProjects/index.d.ts.map +0 -1
- package/dist/platformProjects/index.js.map +0 -1
- package/dist/platformProjects/interface.d.ts.map +0 -1
- package/dist/platformProjects/interface.js.map +0 -1
- package/dist/platformProjects/schemas.d.ts.map +0 -1
- package/dist/platformProjects/schemas.js.map +0 -1
- package/dist/platformProjects/types.d.ts.map +0 -1
- package/dist/platformProjects/types.js.map +0 -1
- package/dist/projects/index.d.ts.map +0 -1
- package/dist/projects/index.js.map +0 -1
- package/dist/projects/interface.d.ts.map +0 -1
- package/dist/projects/interface.js.map +0 -1
- package/dist/projects/schemas.d.ts.map +0 -1
- package/dist/projects/schemas.js.map +0 -1
- package/dist/projects/types.d.ts.map +0 -1
- package/dist/projects/types.js.map +0 -1
- package/dist/shareLinks/index.d.ts.map +0 -1
- package/dist/shareLinks/index.js.map +0 -1
- package/dist/shareLinks/interface.d.ts.map +0 -1
- package/dist/shareLinks/interface.js.map +0 -1
- package/dist/shareLinks/schemas.d.ts.map +0 -1
- package/dist/shareLinks/schemas.js.map +0 -1
- package/dist/shareLinks/types.d.ts.map +0 -1
- package/dist/shareLinks/types.js.map +0 -1
- package/dist/solveCache/interface.d.ts.map +0 -1
- package/dist/solveCache/interface.js.map +0 -1
- package/dist/storage/assetClasses.d.ts.map +0 -1
- package/dist/storage/assetClasses.js.map +0 -1
- package/dist/storage/cacheBust.d.ts.map +0 -1
- package/dist/storage/cacheBust.js.map +0 -1
- package/dist/storage/image.d.ts.map +0 -1
- package/dist/storage/image.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/interface.d.ts.map +0 -1
- package/dist/storage/interface.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/suites/apiTokenStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/apiTokenStoreSuite.js.map +0 -1
- package/dist/testing/suites/authProviderSuite.d.ts.map +0 -1
- package/dist/testing/suites/authProviderSuite.js.map +0 -1
- package/dist/testing/suites/computeServerStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/computeServerStoreSuite.js.map +0 -1
- package/dist/testing/suites/definitionStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/definitionStoreSuite.js.map +0 -1
- package/dist/testing/suites/emailLinkAuthSuite.d.ts.map +0 -1
- package/dist/testing/suites/emailLinkAuthSuite.js.map +0 -1
- package/dist/testing/suites/eventSinkSuite.d.ts.map +0 -1
- package/dist/testing/suites/eventSinkSuite.js.map +0 -1
- package/dist/testing/suites/helpers.d.ts.map +0 -1
- package/dist/testing/suites/helpers.js.map +0 -1
- package/dist/testing/suites/inviteStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/inviteStoreSuite.js.map +0 -1
- package/dist/testing/suites/orgStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/orgStoreSuite.js.map +0 -1
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js.map +0 -1
- package/dist/testing/suites/platformProjectGrantStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/platformProjectGrantStoreSuite.js.map +0 -1
- package/dist/testing/suites/projectStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/projectStoreSuite.js.map +0 -1
- package/dist/testing/suites/shareLinkStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/shareLinkStoreSuite.js.map +0 -1
- package/dist/testing/suites/solveMetricSinkSuite.d.ts.map +0 -1
- package/dist/testing/suites/solveMetricSinkSuite.js.map +0 -1
- package/dist/testing/suites/storageProviderSuite.d.ts.map +0 -1
- package/dist/testing/suites/storageProviderSuite.js.map +0 -1
- package/dist/testing/suites/userProfileStoreSuite.d.ts.map +0 -1
- package/dist/testing/suites/userProfileStoreSuite.js.map +0 -1
- package/dist/userProfile/index.d.ts.map +0 -1
- package/dist/userProfile/index.js.map +0 -1
- package/dist/userProfile/interface.d.ts.map +0 -1
- package/dist/userProfile/interface.js.map +0 -1
- package/dist/userProfile/types.d.ts.map +0 -1
- package/dist/userProfile/types.js.map +0 -1
- package/dist/utils/audit.d.ts.map +0 -1
- package/dist/utils/audit.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
|
@@ -5,12 +5,9 @@
|
|
|
5
5
|
* `countInstanceAdminsExcluding`, plus the §2 sole-`instance_admin`
|
|
6
6
|
* invariant — `set` must return `'last_admin'` when dropping the only admin,
|
|
7
7
|
* and `countInstanceAdminsExcluding` must exclude the named user.
|
|
8
|
-
*
|
|
9
|
-
* These tests used to live on the auth-provider conformance suite when the
|
|
10
|
-
* invariant was enforced there; the move from `IAuthProvider` to
|
|
11
|
-
* `IPlatformPermissionStore` brought them with it.
|
|
12
8
|
*/
|
|
13
9
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { makeSystemCtx } from './helpers.js';
|
|
14
11
|
import { ALL_ORG_PERMISSIONS, ALL_PLATFORM_PERMISSIONS } from '../../index.js';
|
|
15
12
|
export function runPlatformPermissionStoreConformance(opts) {
|
|
16
13
|
const { name, createStore, disableUser, cleanup } = opts;
|
|
@@ -89,12 +86,7 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
89
86
|
it('hasInstanceAdmin returns false on a fresh instance, true after granting', async () => {
|
|
90
87
|
const { store, seedUser } = await createStore();
|
|
91
88
|
const adminId = await seedUser();
|
|
92
|
-
const sysCtx =
|
|
93
|
-
userId: '',
|
|
94
|
-
platformPermissions: [],
|
|
95
|
-
orgPermissions: [],
|
|
96
|
-
system: true
|
|
97
|
-
};
|
|
89
|
+
const sysCtx = makeSystemCtx();
|
|
98
90
|
expect(await store.hasInstanceAdmin(sysCtx)).toBe(false);
|
|
99
91
|
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
100
92
|
expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
|
|
@@ -121,10 +113,8 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
121
113
|
const after = await store.getFor(adminCtx(adminA), adminB);
|
|
122
114
|
expect(after).toContain('instance_admin');
|
|
123
115
|
});
|
|
124
|
-
// The invariant has to survive concurrency, not just sequence
|
|
125
|
-
// demoting each other
|
|
126
|
-
// "another admin exists" and both commit, bricking the instance. Exactly
|
|
127
|
-
// one must win.
|
|
116
|
+
// The invariant has to survive concurrency, not just sequence: two admins
|
|
117
|
+
// demoting each other must not each read the other as the survivor.
|
|
128
118
|
it('concurrent demotions of two admins leave one standing', async () => {
|
|
129
119
|
const { store, seedUser } = await createStore();
|
|
130
120
|
const adminA = await seedUser();
|
|
@@ -138,12 +128,7 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
138
128
|
// One demotion succeeds, the other is refused — never both.
|
|
139
129
|
expect([resA, resB].filter((r) => r === 'ok')).toHaveLength(1);
|
|
140
130
|
expect([resA, resB].filter((r) => r === 'last_admin')).toHaveLength(1);
|
|
141
|
-
const sysCtx =
|
|
142
|
-
userId: '',
|
|
143
|
-
platformPermissions: [],
|
|
144
|
-
orgPermissions: [],
|
|
145
|
-
system: true
|
|
146
|
-
};
|
|
131
|
+
const sysCtx = makeSystemCtx();
|
|
147
132
|
expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
|
|
148
133
|
});
|
|
149
134
|
it('a burst of concurrent demotions never drops the last admin', async () => {
|
|
@@ -159,24 +144,14 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
159
144
|
// Whatever the interleaving, exactly one admin survives.
|
|
160
145
|
expect(results.filter((r) => r === 'ok')).toHaveLength(ids.length - 1);
|
|
161
146
|
expect(results.filter((r) => r === 'last_admin')).toHaveLength(1);
|
|
162
|
-
const sysCtx =
|
|
163
|
-
userId: '',
|
|
164
|
-
platformPermissions: [],
|
|
165
|
-
orgPermissions: [],
|
|
166
|
-
system: true
|
|
167
|
-
};
|
|
147
|
+
const sysCtx = makeSystemCtx();
|
|
168
148
|
expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
|
|
169
149
|
});
|
|
170
150
|
it('claimFirstInstanceAdmin grants on a fresh instance and refuses once claimed', async () => {
|
|
171
151
|
const { store, seedUser } = await createStore();
|
|
172
152
|
const first = await seedUser();
|
|
173
153
|
const second = await seedUser();
|
|
174
|
-
const sysCtx =
|
|
175
|
-
userId: '',
|
|
176
|
-
platformPermissions: [],
|
|
177
|
-
orgPermissions: [],
|
|
178
|
-
system: true
|
|
179
|
-
};
|
|
154
|
+
const sysCtx = makeSystemCtx();
|
|
180
155
|
expect(await store.claimFirstInstanceAdmin(sysCtx, first, ['instance_admin'])).toBe(true);
|
|
181
156
|
expect(await store.getFor(adminCtx(first), first)).toContain('instance_admin');
|
|
182
157
|
// The instance is claimed. A later signer must not become a second
|
|
@@ -193,12 +168,7 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
193
168
|
const ids = [];
|
|
194
169
|
for (let i = 0; i < 4; i++)
|
|
195
170
|
ids.push(await seedUser());
|
|
196
|
-
const sysCtx =
|
|
197
|
-
userId: '',
|
|
198
|
-
platformPermissions: [],
|
|
199
|
-
orgPermissions: [],
|
|
200
|
-
system: true
|
|
201
|
-
};
|
|
171
|
+
const sysCtx = makeSystemCtx();
|
|
202
172
|
const results = await Promise.all(ids.map((id) => store.claimFirstInstanceAdmin(sysCtx, id, ['instance_admin'])));
|
|
203
173
|
expect(results.filter(Boolean)).toHaveLength(1);
|
|
204
174
|
const admins = await store.getForBatch(adminCtx(ids[0]), ids);
|
|
@@ -210,12 +180,7 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
210
180
|
const adminB = await seedUser();
|
|
211
181
|
await store.set(adminCtx(adminA), adminA, ['instance_admin']);
|
|
212
182
|
await store.set(adminCtx(adminA), adminB, ['instance_admin']);
|
|
213
|
-
const sysCtx =
|
|
214
|
-
userId: '',
|
|
215
|
-
platformPermissions: [],
|
|
216
|
-
orgPermissions: [],
|
|
217
|
-
system: true
|
|
218
|
-
};
|
|
183
|
+
const sysCtx = makeSystemCtx();
|
|
219
184
|
// Excluding adminA: one admin (adminB) remains
|
|
220
185
|
expect(await store.countInstanceAdminsExcluding(sysCtx, adminA)).toBe(1);
|
|
221
186
|
// Excluding adminB: one admin (adminA) remains
|
|
@@ -225,23 +190,13 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
225
190
|
const { store, seedUser } = await createStore();
|
|
226
191
|
const adminId = await seedUser();
|
|
227
192
|
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
228
|
-
const sysCtx =
|
|
229
|
-
userId: '',
|
|
230
|
-
platformPermissions: [],
|
|
231
|
-
orgPermissions: [],
|
|
232
|
-
system: true
|
|
233
|
-
};
|
|
193
|
+
const sysCtx = makeSystemCtx();
|
|
234
194
|
expect(await store.countInstanceAdminsExcluding(sysCtx, adminId)).toBe(0);
|
|
235
195
|
});
|
|
236
196
|
itWithDisable('hasInstanceAdmin ignores disabled admins', async () => {
|
|
237
197
|
const { store, seedUser } = await createStore();
|
|
238
198
|
const adminId = await seedUser();
|
|
239
|
-
const sysCtx =
|
|
240
|
-
userId: '',
|
|
241
|
-
platformPermissions: [],
|
|
242
|
-
orgPermissions: [],
|
|
243
|
-
system: true
|
|
244
|
-
};
|
|
199
|
+
const sysCtx = makeSystemCtx();
|
|
245
200
|
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
246
201
|
expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
|
|
247
202
|
await disableUser(adminId);
|
|
@@ -253,12 +208,7 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
253
208
|
const adminB = await seedUser();
|
|
254
209
|
await store.set(adminCtx(adminA), adminA, ['instance_admin']);
|
|
255
210
|
await store.set(adminCtx(adminA), adminB, ['instance_admin']);
|
|
256
|
-
const sysCtx =
|
|
257
|
-
userId: '',
|
|
258
|
-
platformPermissions: [],
|
|
259
|
-
orgPermissions: [],
|
|
260
|
-
system: true
|
|
261
|
-
};
|
|
211
|
+
const sysCtx = makeSystemCtx();
|
|
262
212
|
await disableUser(adminB);
|
|
263
213
|
// adminB holds the grant but is disabled — must not count as the
|
|
264
214
|
// "other admin" that would allow demoting/disabling adminA.
|
|
@@ -312,4 +262,3 @@ export function runPlatformPermissionStoreConformance(opts) {
|
|
|
312
262
|
});
|
|
313
263
|
});
|
|
314
264
|
}
|
|
315
|
-
//# sourceMappingURL=platformPermissionStoreSuite.js.map
|
|
@@ -17,4 +17,3 @@ export interface PlatformProjectGrantStoreConformanceOptions {
|
|
|
17
17
|
seedProject?: (projectId: string) => Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
export declare function runPlatformProjectGrantStoreConformance(opts: PlatformProjectGrantStoreConformanceOptions): void;
|
|
20
|
-
//# sourceMappingURL=platformProjectGrantStoreSuite.d.ts.map
|
|
@@ -92,10 +92,6 @@ export function runPlatformProjectGrantStoreConformance(opts) {
|
|
|
92
92
|
const got = await store.listByProject(SYSTEM_CONTEXT, projectId);
|
|
93
93
|
expect(got).toHaveLength(0);
|
|
94
94
|
});
|
|
95
|
-
it('delete on unknown id throws', async () => {
|
|
96
|
-
const store = await createStore();
|
|
97
|
-
await expect(store.delete(SYSTEM_CONTEXT, makeUuid())).rejects.toThrow();
|
|
98
|
-
});
|
|
99
95
|
it('deleteByProject removes every grant for that project', async () => {
|
|
100
96
|
const store = await createStore();
|
|
101
97
|
const target = await seed();
|
|
@@ -131,4 +127,3 @@ export function runPlatformProjectGrantStoreConformance(opts) {
|
|
|
131
127
|
});
|
|
132
128
|
});
|
|
133
129
|
}
|
|
134
|
-
//# sourceMappingURL=platformProjectGrantStoreSuite.js.map
|
|
@@ -24,8 +24,12 @@ export interface ProjectStoreConformanceOptions {
|
|
|
24
24
|
* actually stored — callers use that, not the suggested one.
|
|
25
25
|
*/
|
|
26
26
|
seedUser?: SeedUserFn;
|
|
27
|
+
/**
|
|
28
|
+
* Seeds the other party in the ctx-isolation cases. Needed where `seedUser` makes
|
|
29
|
+
* instance admins, who see everything by design.
|
|
30
|
+
*/
|
|
31
|
+
seedOutsider?: SeedUserFn;
|
|
27
32
|
/** If true, run ctx-isolation tests (adapters with row-level security). */
|
|
28
33
|
ctxIsolation?: boolean;
|
|
29
34
|
}
|
|
30
35
|
export declare function runProjectStoreConformance(opts: ProjectStoreConformanceOptions): void;
|
|
31
|
-
//# sourceMappingURL=projectStoreSuite.d.ts.map
|
|
@@ -32,8 +32,9 @@ function member(projectId, userId, role) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
export function runProjectStoreConformance(opts) {
|
|
35
|
-
const { name, createStore: rawCreateStore, seedUser = noopSeedUser, ctxIsolation = false } = opts;
|
|
35
|
+
const { name, createStore: rawCreateStore, seedUser = noopSeedUser, seedOutsider = seedUser, ctxIsolation = false } = opts;
|
|
36
36
|
const { seed, ctx, registerToken } = makeSeedHelpers(seedUser);
|
|
37
|
+
const outsider = makeSeedHelpers(seedOutsider);
|
|
37
38
|
const createStore = async () => {
|
|
38
39
|
const result = await rawCreateStore();
|
|
39
40
|
if (result.ownerSessionToken)
|
|
@@ -317,22 +318,21 @@ export function runProjectStoreConformance(opts) {
|
|
|
317
318
|
it('ctx isolation: projects created by one user are not visible to another', async () => {
|
|
318
319
|
const { store, orgId } = await createStore();
|
|
319
320
|
const userA = await seed();
|
|
320
|
-
const userB = await seed();
|
|
321
|
+
const userB = await outsider.seed();
|
|
321
322
|
const pA = project(orgId, userA);
|
|
322
323
|
await store.createProject(ctx(userA), pA);
|
|
323
|
-
const page = await store.listProjects(ctx(userB), orgId);
|
|
324
|
+
const page = await store.listProjects(outsider.ctx(userB), orgId);
|
|
324
325
|
expect(page.items.map((p) => p.id)).not.toContain(pA.id);
|
|
325
326
|
});
|
|
326
327
|
it('ctx isolation: user cannot get a project they do not own', async () => {
|
|
327
328
|
const { store, orgId } = await createStore();
|
|
328
329
|
const userA = await seed();
|
|
329
|
-
const userB = await seed();
|
|
330
|
+
const userB = await outsider.seed();
|
|
330
331
|
const pA = project(orgId, userA);
|
|
331
332
|
await store.createProject(ctx(userA), pA);
|
|
332
|
-
const got = await store.getProject(ctx(userB), pA.id);
|
|
333
|
+
const got = await store.getProject(outsider.ctx(userB), pA.id);
|
|
333
334
|
expect(got).toBeNull();
|
|
334
335
|
});
|
|
335
336
|
}
|
|
336
337
|
});
|
|
337
338
|
}
|
|
338
|
-
//# sourceMappingURL=projectStoreSuite.js.map
|
|
@@ -36,6 +36,7 @@ export interface ShareLinkStoreConformanceOptions {
|
|
|
36
36
|
* `definitions` row + parent project). Local adapters can return stub ids.
|
|
37
37
|
*/
|
|
38
38
|
createScope?: () => Promise<ShareLinkTestScope> | ShareLinkTestScope;
|
|
39
|
+
/** The scope seeds `orgId` and `otherOrgId`, so the `listByOrg` cases can run. */
|
|
40
|
+
orgRoster?: boolean;
|
|
39
41
|
}
|
|
40
42
|
export declare function runShareLinkStoreConformance(opts: ShareLinkStoreConformanceOptions): void;
|
|
41
|
-
//# sourceMappingURL=shareLinkStoreSuite.d.ts.map
|
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
* local adapter can stub.
|
|
8
8
|
*/
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
|
|
16
|
-
}
|
|
10
|
+
import { SYSTEM_CONTEXT } from '../../context.js';
|
|
11
|
+
import { makeSeedHelpers, makeUuid } from './helpers.js';
|
|
12
|
+
// `scopeFor` registers the owner's token, so `ctx` runs as them on an adapter with
|
|
13
|
+
// DB-side auth (Supabase RLS). This suite seeds nobody itself.
|
|
14
|
+
const { ctx, registerToken } = makeSeedHelpers(async (userId) => ({ userId }));
|
|
17
15
|
const DEFAULT_SCOPE = {
|
|
18
16
|
ownerId: 'user-1',
|
|
19
17
|
definitionId: 'def-1',
|
|
@@ -37,11 +35,11 @@ function link(scope, overrides = {}) {
|
|
|
37
35
|
};
|
|
38
36
|
}
|
|
39
37
|
export function runShareLinkStoreConformance(opts) {
|
|
40
|
-
const { name, createStore, createScope } = opts;
|
|
38
|
+
const { name, createStore, createScope, orgRoster = false } = opts;
|
|
41
39
|
const scopeFor = async () => {
|
|
42
40
|
const s = createScope ? await createScope() : DEFAULT_SCOPE;
|
|
43
41
|
if (s.ownerSessionToken)
|
|
44
|
-
|
|
42
|
+
registerToken(s.ownerId, s.ownerSessionToken);
|
|
45
43
|
return s;
|
|
46
44
|
};
|
|
47
45
|
describe(`IShareLinkStore conformance: ${name}`, () => {
|
|
@@ -132,15 +130,12 @@ export function runShareLinkStoreConformance(opts) {
|
|
|
132
130
|
const got = await store.getByTokenHash(ctx(scope.ownerId), 'not-yet-expired');
|
|
133
131
|
expect(got?.id).toBe(l.id);
|
|
134
132
|
});
|
|
135
|
-
// The org roster is the only read that answers "what reaches this
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
describe('listByOrg', () => {
|
|
133
|
+
// The org roster is the only read that answers "what reaches this tenant's
|
|
134
|
+
// data right now?". `skipIf` rather than an omitted block, so an adapter that
|
|
135
|
+
// doesn't seed an org shows as skipped instead of silently covering nothing.
|
|
136
|
+
describe.skipIf(!orgRoster)('listByOrg', () => {
|
|
140
137
|
it('returns links across every definition in the org, newest first', async () => {
|
|
141
138
|
const scope = await scopeFor();
|
|
142
|
-
if (!scope.orgId)
|
|
143
|
-
return;
|
|
144
139
|
const store = await createStore();
|
|
145
140
|
const a = link(scope, { createdAt: '2026-04-01T00:00:00.000Z' });
|
|
146
141
|
const b = link(scope, {
|
|
@@ -154,8 +149,6 @@ export function runShareLinkStoreConformance(opts) {
|
|
|
154
149
|
});
|
|
155
150
|
it('names the parent definition and project on every row', async () => {
|
|
156
151
|
const scope = await scopeFor();
|
|
157
|
-
if (!scope.orgId)
|
|
158
|
-
return;
|
|
159
152
|
const store = await createStore();
|
|
160
153
|
const l = link(scope);
|
|
161
154
|
await store.create(ctx(scope.ownerId), l);
|
|
@@ -169,8 +162,6 @@ export function runShareLinkStoreConformance(opts) {
|
|
|
169
162
|
});
|
|
170
163
|
it('never carries tokenHash — the roster spans the whole tenant', async () => {
|
|
171
164
|
const scope = await scopeFor();
|
|
172
|
-
if (!scope.orgId)
|
|
173
|
-
return;
|
|
174
165
|
const store = await createStore();
|
|
175
166
|
await store.create(ctx(scope.ownerId), link(scope));
|
|
176
167
|
const page = await store.listByOrg(ctx(scope.ownerId), scope.orgId, { limit: 100 });
|
|
@@ -181,8 +172,6 @@ export function runShareLinkStoreConformance(opts) {
|
|
|
181
172
|
});
|
|
182
173
|
it('excludes revoked and expired links', async () => {
|
|
183
174
|
const scope = await scopeFor();
|
|
184
|
-
if (!scope.orgId)
|
|
185
|
-
return;
|
|
186
175
|
const store = await createStore();
|
|
187
176
|
const revoked = link(scope);
|
|
188
177
|
const expired = link(scope, { expiresAt: new Date(Date.now() - 60_000).toISOString() });
|
|
@@ -199,14 +188,23 @@ export function runShareLinkStoreConformance(opts) {
|
|
|
199
188
|
});
|
|
200
189
|
it('does not leak links into another org', async () => {
|
|
201
190
|
const scope = await scopeFor();
|
|
202
|
-
if (!scope.
|
|
203
|
-
|
|
191
|
+
if (!scope.otherOrgId)
|
|
192
|
+
throw new Error('orgRoster requires scope.otherOrgId');
|
|
204
193
|
const store = await createStore();
|
|
205
194
|
await store.create(ctx(scope.ownerId), link(scope));
|
|
206
195
|
const page = await store.listByOrg(ctx(scope.ownerId), scope.otherOrgId, { limit: 100 });
|
|
207
196
|
expect(page.items).toHaveLength(0);
|
|
208
197
|
});
|
|
209
198
|
});
|
|
199
|
+
it('tryIncrementSolveCount holds the cap under concurrent solves', async () => {
|
|
200
|
+
const store = await createStore();
|
|
201
|
+
const scope = await scopeFor();
|
|
202
|
+
const l = link(scope, { maxSolves: 5 });
|
|
203
|
+
await store.create(ctx(scope.ownerId), l);
|
|
204
|
+
const results = await Promise.all(Array.from({ length: 20 }, () => store.tryIncrementSolveCount(SYSTEM_CONTEXT, l.id)));
|
|
205
|
+
expect(results.filter((n) => n !== null)).toHaveLength(5);
|
|
206
|
+
expect((await store.getById(ctx(scope.ownerId), l.id))?.solveCount).toBe(5);
|
|
207
|
+
});
|
|
210
208
|
it('revoke is idempotent (no error on double-revoke)', async () => {
|
|
211
209
|
const store = await createStore();
|
|
212
210
|
const scope = await scopeFor();
|
|
@@ -251,4 +249,3 @@ export function runShareLinkStoreConformance(opts) {
|
|
|
251
249
|
});
|
|
252
250
|
});
|
|
253
251
|
}
|
|
254
|
-
//# sourceMappingURL=shareLinkStoreSuite.js.map
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* a backend failure — the compute route depends on that to keep serving the
|
|
11
11
|
* response after a solve.
|
|
12
12
|
*/
|
|
13
|
+
import { makeSystemCtx } from './helpers.js';
|
|
13
14
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
14
15
|
function sysCtx(userId) {
|
|
15
|
-
return
|
|
16
|
+
return makeSystemCtx(userId);
|
|
16
17
|
}
|
|
17
18
|
function baseMetric(overrides = {}) {
|
|
18
19
|
return {
|
|
@@ -95,4 +96,3 @@ export function runSolveMetricSinkConformance(opts) {
|
|
|
95
96
|
}
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
|
-
//# sourceMappingURL=solveMetricSinkSuite.js.map
|
|
@@ -12,4 +12,3 @@ export interface StorageProviderConformanceOptions {
|
|
|
12
12
|
createStorage: () => Promise<IStorageProvider> | IStorageProvider;
|
|
13
13
|
}
|
|
14
14
|
export declare function runStorageProviderConformance(opts: StorageProviderConformanceOptions): void;
|
|
15
|
-
//# sourceMappingURL=storageProviderSuite.d.ts.map
|
|
@@ -24,6 +24,10 @@ function checksum(data) {
|
|
|
24
24
|
export function runStorageProviderConformance(opts) {
|
|
25
25
|
const { name, createStorage } = opts;
|
|
26
26
|
describe(`IStorageProvider conformance: ${name}`, () => {
|
|
27
|
+
it('put returns the path it stored at', async () => {
|
|
28
|
+
const storage = await createStorage();
|
|
29
|
+
expect(await storage.put('test/plain.txt', bytes('data'), 'text/plain')).toBe('test/plain.txt');
|
|
30
|
+
});
|
|
27
31
|
it('put + get returns the stored data', async () => {
|
|
28
32
|
const storage = await createStorage();
|
|
29
33
|
const data = bytes('Hello, World!');
|
|
@@ -77,6 +81,13 @@ export function runStorageProviderConformance(opts) {
|
|
|
77
81
|
expect(meta1).toBeNull();
|
|
78
82
|
expect(file2).toBeTruthy();
|
|
79
83
|
});
|
|
84
|
+
it('deletePrefix refuses a prefix that names the whole store', async () => {
|
|
85
|
+
const storage = await createStorage();
|
|
86
|
+
await storage.put('test/file.txt', bytes('keep'));
|
|
87
|
+
await expect(storage.deletePrefix('')).rejects.toThrow();
|
|
88
|
+
await expect(storage.deletePrefix('/')).rejects.toThrow();
|
|
89
|
+
expect(await storage.get('test/file.txt')).toEqual(bytes('keep'));
|
|
90
|
+
});
|
|
80
91
|
it('deletePrefix with no matches is a no-op', async () => {
|
|
81
92
|
const storage = await createStorage();
|
|
82
93
|
await storage.put('other/file.txt', bytes('data'));
|
|
@@ -136,18 +147,13 @@ export function runStorageProviderConformance(opts) {
|
|
|
136
147
|
// Image transcoding
|
|
137
148
|
// ============================================================================
|
|
138
149
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
// of what the user uploaded. Otherwise a provider that skipped this would
|
|
142
|
-
// produce a pipeline that looks correct until someone uploads a PNG.
|
|
150
|
+
// Part of the `IStorageProvider.put` contract: images land as WebP at a
|
|
151
|
+
// rewritten path, whatever the user uploaded.
|
|
143
152
|
it('transcodes png input to webp (path rewritten, magic bytes flipped)', async () => {
|
|
144
153
|
const storage = await createStorage();
|
|
145
154
|
// Minimal 1×1 transparent PNG (68 bytes): 8-byte signature + IHDR + IDAT + IEND.
|
|
146
155
|
// The IDAT is a real zlib stream of one scanline (filter 0 + RGBA 0,0,0,0) with a
|
|
147
|
-
// correct Adler-32. Don't hand-tweak these bytes
|
|
148
|
-
// truncated IDAT that older libpng tolerated and sharp >=0.35 rejects outright
|
|
149
|
-
// ("vipspng: libpng read error"). Regenerate with zlib.deflateSync if it ever needs
|
|
150
|
-
// to change.
|
|
156
|
+
// correct Adler-32. Don't hand-tweak these bytes: regenerate with zlib.deflateSync.
|
|
151
157
|
const pngBytes = Uint8Array.from([
|
|
152
158
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
153
159
|
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
|
|
@@ -155,7 +161,7 @@ export function runStorageProviderConformance(opts) {
|
|
|
155
161
|
0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x7a, 0x5e, 0xab, 0x3f, 0x00, 0x00, 0x00, 0x00,
|
|
156
162
|
0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
|
|
157
163
|
]);
|
|
158
|
-
await storage.put('covers/tiny.png', pngBytes, 'image/png');
|
|
164
|
+
expect(await storage.put('covers/tiny.png', pngBytes, 'image/png')).toBe('covers/tiny.webp');
|
|
159
165
|
// Image transcoding rewrites the path to .webp — the original path is empty.
|
|
160
166
|
const atPng = await storage.get('covers/tiny.png');
|
|
161
167
|
expect(atPng).toBeNull();
|
|
@@ -185,4 +191,3 @@ export function runStorageProviderConformance(opts) {
|
|
|
185
191
|
});
|
|
186
192
|
});
|
|
187
193
|
}
|
|
188
|
-
//# sourceMappingURL=storageProviderSuite.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Adapter conformance suite for IUserProfileStore.
|
|
3
3
|
*
|
|
4
4
|
* Tests the user-profile mutations (display name, starred definitions,
|
|
5
|
-
* recent runs)
|
|
5
|
+
* recent runs). Adapters must seed
|
|
6
6
|
* a real user before each test since profile mutations target a userId.
|
|
7
7
|
*/
|
|
8
8
|
import type { IUserProfileStore } from '../../userProfile/interface.js';
|
|
@@ -21,4 +21,3 @@ export interface UserProfileStoreConformanceOptions {
|
|
|
21
21
|
}>;
|
|
22
22
|
}
|
|
23
23
|
export declare function runUserProfileStoreConformance(opts: UserProfileStoreConformanceOptions): void;
|
|
24
|
-
//# sourceMappingURL=userProfileStoreSuite.d.ts.map
|
|
@@ -2,20 +2,56 @@
|
|
|
2
2
|
* Adapter conformance suite for IUserProfileStore.
|
|
3
3
|
*
|
|
4
4
|
* Tests the user-profile mutations (display name, starred definitions,
|
|
5
|
-
* recent runs)
|
|
5
|
+
* recent runs). Adapters must seed
|
|
6
6
|
* a real user before each test since profile mutations target a userId.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, it, expect } from 'vitest';
|
|
9
9
|
import { SYSTEM_CONTEXT } from '../../context.js';
|
|
10
|
-
import { makeUuid } from './helpers.js';
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// uses SYSTEM_CONTEXT throughout so it doesn't need to mint per-user ctxs;
|
|
14
|
-
// the per-user authorization is exercised in adapter-specific tests.
|
|
10
|
+
import { makeCtx, makeUuid } from './helpers.js';
|
|
11
|
+
// SYSTEM_CONTEXT, so most cases don't need a ctx minted per user. The scoping case
|
|
12
|
+
// below builds its own.
|
|
15
13
|
const ctx = SYSTEM_CONTEXT;
|
|
16
14
|
export function runUserProfileStoreConformance(opts) {
|
|
17
15
|
const { name, createStore } = opts;
|
|
18
16
|
describe(`IUserProfileStore conformance: ${name}`, () => {
|
|
17
|
+
it('a user cannot read or change someone else through a plain ctx', async () => {
|
|
18
|
+
const { store, seedUser } = await createStore();
|
|
19
|
+
const victim = await seedUser('victim@example.com');
|
|
20
|
+
const other = makeCtx(makeUuid(), { platformPermissions: [] });
|
|
21
|
+
await expect(store.getProfile(other, victim.id)).rejects.toThrow();
|
|
22
|
+
await expect(store.updateProfile(other, victim.id, { displayName: 'Hacked' })).rejects.toThrow();
|
|
23
|
+
});
|
|
24
|
+
it('recordRun keeps one entry per definition, newest first', async () => {
|
|
25
|
+
const { store, seedUser } = await createStore();
|
|
26
|
+
const user = await seedUser('dedupe@example.com');
|
|
27
|
+
const run = (definitionId, timestamp) => ({
|
|
28
|
+
definitionId,
|
|
29
|
+
runId: makeUuid(),
|
|
30
|
+
definitionName: 'D',
|
|
31
|
+
timestamp
|
|
32
|
+
});
|
|
33
|
+
const a = makeUuid();
|
|
34
|
+
const b = makeUuid();
|
|
35
|
+
await store.recordRun(ctx, user.id, run(a, '2026-01-01T00:00:00.000Z'));
|
|
36
|
+
await store.recordRun(ctx, user.id, run(b, '2026-01-02T00:00:00.000Z'));
|
|
37
|
+
await store.recordRun(ctx, user.id, run(a, '2026-01-03T00:00:00.000Z'));
|
|
38
|
+
const profile = await store.getProfile(ctx, user.id);
|
|
39
|
+
expect(profile?.recentRuns.map((r) => r.definitionId)).toEqual([a, b]);
|
|
40
|
+
});
|
|
41
|
+
it('recordRun caps the list', async () => {
|
|
42
|
+
const { store, seedUser } = await createStore();
|
|
43
|
+
const user = await seedUser('cap@example.com');
|
|
44
|
+
for (let i = 0; i < 30; i++) {
|
|
45
|
+
await store.recordRun(ctx, user.id, {
|
|
46
|
+
definitionId: makeUuid(),
|
|
47
|
+
runId: makeUuid(),
|
|
48
|
+
definitionName: 'D',
|
|
49
|
+
timestamp: new Date(i).toISOString()
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const profile = await store.getProfile(ctx, user.id);
|
|
53
|
+
expect(profile?.recentRuns.length).toBeLessThan(30);
|
|
54
|
+
});
|
|
19
55
|
it('getProfile returns null for unknown user', async () => {
|
|
20
56
|
const { store } = await createStore();
|
|
21
57
|
const profile = await store.getProfile(ctx, makeUuid());
|
|
@@ -102,4 +138,3 @@ export function runUserProfileStoreConformance(opts) {
|
|
|
102
138
|
});
|
|
103
139
|
});
|
|
104
140
|
}
|
|
105
|
-
//# sourceMappingURL=userProfileStoreSuite.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { IUserProfileStore } from './interface.js';
|
|
2
2
|
export type { UserProfile, RecentRun } from './types.js';
|
|
3
|
-
export { emptyProfile } from './types.js';
|
|
4
|
-
|
|
3
|
+
export { MAX_RECENT_RUNS, emptyProfile } from './types.js';
|
|
4
|
+
export { assertCanAccessProfile } from '../context.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { emptyProfile } from './types.js';
|
|
2
|
-
|
|
1
|
+
export { MAX_RECENT_RUNS, emptyProfile } from './types.js';
|
|
2
|
+
export { assertCanAccessProfile } from '../context.js';
|
|
@@ -6,11 +6,11 @@ import type { RecentRun, UserProfile } from './types.js';
|
|
|
6
6
|
* providers (Entra, Supabase Auth) don't have to model fields they can't
|
|
7
7
|
* persist.
|
|
8
8
|
*
|
|
9
|
-
* Adapters must scope by `ctx`: a caller cannot read
|
|
10
|
-
* user's profile via the `userId` argument.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Adapters must scope `getProfile` and the writes by `ctx`: a caller cannot read
|
|
10
|
+
* or modify another user's profile via the `userId` argument. `getProfiles` is
|
|
11
|
+
* unscoped by design, because rosters show other people's names, so its callers
|
|
12
|
+
* decide who may see the list. Pass `SYSTEM_CONTEXT` for trusted server flows
|
|
13
|
+
* (auto-seed on signup, janitors).
|
|
14
14
|
*/
|
|
15
15
|
export interface IUserProfileStore {
|
|
16
16
|
/** Returns null when no profile row exists — treat as `emptyProfile(id)`. */
|
|
@@ -30,4 +30,3 @@ export interface IUserProfileStore {
|
|
|
30
30
|
*/
|
|
31
31
|
recordRun(ctx: RequestContext, userId: string, run: RecentRun): Promise<UserManagementResult>;
|
|
32
32
|
}
|
|
33
|
-
//# sourceMappingURL=interface.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** How many entries `recordRun` keeps per user. */
|
|
2
|
+
export declare const MAX_RECENT_RUNS = 20;
|
|
1
3
|
export interface RecentRun {
|
|
2
4
|
definitionId: string;
|
|
3
5
|
runId: string;
|
|
@@ -14,4 +16,3 @@ export interface UserProfile {
|
|
|
14
16
|
recentRuns: RecentRun[];
|
|
15
17
|
}
|
|
16
18
|
export declare function emptyProfile(userId: string): UserProfile;
|
|
17
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/utils/audit.d.ts
CHANGED
package/dist/utils/audit.js
CHANGED
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/platform",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
|
+
"description": "Interfaces, access rules and conformance suites for Selva platform providers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
|
7
7
|
"homepage": "https://selva.dev",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"node": ">=24.0.0"
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
28
29
|
"main": "./dist/index.js",
|
|
29
30
|
"types": "./dist/index.d.ts",
|
|
30
31
|
"exports": {
|
|
@@ -131,8 +132,8 @@
|
|
|
131
132
|
"!**/*.spec.*"
|
|
132
133
|
],
|
|
133
134
|
"dependencies": {
|
|
134
|
-
"zod": "^4.6.
|
|
135
|
-
"@selvajs/schemas": "^5.3.0-beta.
|
|
135
|
+
"zod": "^4.6.7",
|
|
136
|
+
"@selvajs/schemas": "^5.3.0-beta.1"
|
|
136
137
|
},
|
|
137
138
|
"peerDependencies": {
|
|
138
139
|
"sharp": "^0.34.0 || ^0.35.0",
|
|
@@ -147,9 +148,9 @@
|
|
|
147
148
|
}
|
|
148
149
|
},
|
|
149
150
|
"devDependencies": {
|
|
150
|
-
"sharp": "^0.35.
|
|
151
|
+
"sharp": "^0.35.5",
|
|
151
152
|
"typescript": "~6.0.3",
|
|
152
|
-
"vitest": "^5.0.
|
|
153
|
+
"vitest": "^5.0.5",
|
|
153
154
|
"@selvajs/config": "0.0.4"
|
|
154
155
|
},
|
|
155
156
|
"scripts": {
|