@selvajs/platform 0.14.3-beta.0 → 0.15.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +1 -1
  2. package/dist/computeServer/index.d.ts +1 -1
  3. package/dist/computeServer/index.d.ts.map +1 -1
  4. package/dist/computeServer/interface.d.ts +29 -1
  5. package/dist/computeServer/interface.d.ts.map +1 -1
  6. package/dist/computeServer/types.d.ts +12 -1
  7. package/dist/computeServer/types.d.ts.map +1 -1
  8. package/dist/computeServer/types.js.map +1 -1
  9. package/dist/data/index.d.ts +2 -1
  10. package/dist/data/index.d.ts.map +1 -1
  11. package/dist/data/index.js +1 -1
  12. package/dist/data/index.js.map +1 -1
  13. package/dist/data/interface.d.ts +69 -8
  14. package/dist/data/interface.d.ts.map +1 -1
  15. package/dist/data/interface.js +7 -1
  16. package/dist/data/interface.js.map +1 -1
  17. package/dist/definitions/interface.d.ts +16 -1
  18. package/dist/definitions/interface.d.ts.map +1 -1
  19. package/dist/definitions/schemas.d.ts +1 -0
  20. package/dist/definitions/schemas.d.ts.map +1 -1
  21. package/dist/definitions/schemas.js +8 -0
  22. package/dist/definitions/schemas.js.map +1 -1
  23. package/dist/definitions/types.d.ts +24 -1
  24. package/dist/definitions/types.d.ts.map +1 -1
  25. package/dist/events/index.d.ts +1 -1
  26. package/dist/events/index.d.ts.map +1 -1
  27. package/dist/events/index.js +1 -1
  28. package/dist/events/index.js.map +1 -1
  29. package/dist/events/interface.d.ts +30 -0
  30. package/dist/events/interface.d.ts.map +1 -1
  31. package/dist/events/interface.js +7 -0
  32. package/dist/events/interface.js.map +1 -1
  33. package/dist/index.d.ts +9 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/logging/interface.d.ts +82 -0
  38. package/dist/logging/interface.d.ts.map +1 -0
  39. package/dist/logging/interface.js +33 -0
  40. package/dist/logging/interface.js.map +1 -0
  41. package/dist/metrics/interface.d.ts +6 -0
  42. package/dist/metrics/interface.d.ts.map +1 -1
  43. package/dist/metrics/interface.js.map +1 -1
  44. package/dist/organizations/__tests__/slug-schema.test.d.ts +2 -0
  45. package/dist/organizations/__tests__/slug-schema.test.d.ts.map +1 -0
  46. package/dist/organizations/__tests__/slug-schema.test.js +38 -0
  47. package/dist/organizations/__tests__/slug-schema.test.js.map +1 -0
  48. package/dist/organizations/index.d.ts +1 -1
  49. package/dist/organizations/index.d.ts.map +1 -1
  50. package/dist/organizations/index.js +1 -1
  51. package/dist/organizations/index.js.map +1 -1
  52. package/dist/organizations/schemas.d.ts +16 -2
  53. package/dist/organizations/schemas.d.ts.map +1 -1
  54. package/dist/organizations/schemas.js +32 -2
  55. package/dist/organizations/schemas.js.map +1 -1
  56. package/dist/solveCache/interface.d.ts +86 -0
  57. package/dist/solveCache/interface.d.ts.map +1 -0
  58. package/dist/solveCache/interface.js +12 -0
  59. package/dist/solveCache/interface.js.map +1 -0
  60. package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
  61. package/dist/testing/suites/definitionStoreSuite.js +57 -0
  62. package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
  63. package/dist/testing/suites/eventSinkSuite.js +1 -0
  64. package/dist/testing/suites/eventSinkSuite.js.map +1 -1
  65. package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
  66. package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
  67. package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
  68. package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
  69. package/dist/testing/suites/solveMetricSinkSuite.d.ts +5 -0
  70. package/dist/testing/suites/solveMetricSinkSuite.d.ts.map +1 -1
  71. package/dist/testing/suites/solveMetricSinkSuite.js +17 -8
  72. package/dist/testing/suites/solveMetricSinkSuite.js.map +1 -1
  73. package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
  74. package/dist/testing/suites/storageProviderSuite.js +23 -2
  75. package/dist/testing/suites/storageProviderSuite.js.map +1 -1
  76. package/package.json +9 -24
  77. package/src/access/index.ts +0 -23
  78. package/src/access/rules.ts +0 -276
  79. package/src/auth/index.ts +0 -8
  80. package/src/auth/interface.ts +0 -279
  81. package/src/auth/types.ts +0 -32
  82. package/src/bindings/index.ts +0 -2
  83. package/src/bindings/interface.ts +0 -61
  84. package/src/computeServer/index.ts +0 -28
  85. package/src/computeServer/interface.ts +0 -73
  86. package/src/computeServer/secrets.ts +0 -102
  87. package/src/computeServer/types.ts +0 -74
  88. package/src/computeServer/utils.ts +0 -112
  89. package/src/config.ts +0 -99
  90. package/src/context.ts +0 -80
  91. package/src/data/index.ts +0 -1
  92. package/src/data/interface.ts +0 -70
  93. package/src/definitions/index.ts +0 -26
  94. package/src/definitions/interface.ts +0 -87
  95. package/src/definitions/paths.ts +0 -31
  96. package/src/definitions/schemas.ts +0 -64
  97. package/src/definitions/types.ts +0 -111
  98. package/src/errors/interface.ts +0 -45
  99. package/src/errors.ts +0 -16
  100. package/src/events/audit.ts +0 -67
  101. package/src/events/index.ts +0 -9
  102. package/src/events/interface.ts +0 -102
  103. package/src/index.ts +0 -247
  104. package/src/invites/index.ts +0 -2
  105. package/src/invites/interface.ts +0 -38
  106. package/src/invites/types.ts +0 -39
  107. package/src/metrics/interface.ts +0 -86
  108. package/src/organizations/index.ts +0 -23
  109. package/src/organizations/interface.ts +0 -68
  110. package/src/organizations/paths.ts +0 -45
  111. package/src/organizations/schemas.ts +0 -61
  112. package/src/organizations/types.ts +0 -41
  113. package/src/pagination.ts +0 -45
  114. package/src/permissions/index.ts +0 -3
  115. package/src/permissions/interface.ts +0 -60
  116. package/src/permissions/types.ts +0 -21
  117. package/src/platformProjects/index.ts +0 -3
  118. package/src/platformProjects/interface.ts +0 -27
  119. package/src/platformProjects/schemas.ts +0 -13
  120. package/src/platformProjects/types.ts +0 -22
  121. package/src/projects/index.ts +0 -4
  122. package/src/projects/interface.ts +0 -58
  123. package/src/projects/schemas.ts +0 -28
  124. package/src/projects/types.ts +0 -36
  125. package/src/shareLinks/index.ts +0 -5
  126. package/src/shareLinks/interface.ts +0 -35
  127. package/src/shareLinks/schemas.ts +0 -21
  128. package/src/shareLinks/types.ts +0 -34
  129. package/src/storage/assetClasses.ts +0 -134
  130. package/src/storage/cacheBust.ts +0 -26
  131. package/src/storage/image.ts +0 -82
  132. package/src/storage/index.ts +0 -12
  133. package/src/storage/interface.ts +0 -27
  134. package/src/testing/index.ts +0 -47
  135. package/src/testing/suites/authProviderSuite.ts +0 -207
  136. package/src/testing/suites/computeServerStoreSuite.ts +0 -195
  137. package/src/testing/suites/definitionStoreSuite.ts +0 -630
  138. package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
  139. package/src/testing/suites/eventSinkSuite.ts +0 -379
  140. package/src/testing/suites/helpers.ts +0 -87
  141. package/src/testing/suites/inviteStoreSuite.ts +0 -143
  142. package/src/testing/suites/orgStoreSuite.ts +0 -429
  143. package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
  144. package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
  145. package/src/testing/suites/projectStoreSuite.ts +0 -381
  146. package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
  147. package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
  148. package/src/testing/suites/storageProviderSuite.ts +0 -196
  149. package/src/testing/suites/userProfileStoreSuite.ts +0 -134
  150. package/src/userProfile/index.ts +0 -3
  151. package/src/userProfile/interface.ts +0 -48
  152. package/src/userProfile/types.ts +0 -24
  153. package/src/utils/audit.ts +0 -46
  154. package/src/utils/index.ts +0 -2
@@ -1,134 +0,0 @@
1
- import { ALL_ORG_ASSET_KINDS } from '../organizations/schemas.js';
2
- import { COVER_IMAGE_EXTENSIONS } from '../definitions/types.js';
3
-
4
- /**
5
- * How a stored asset is allowed to be read. This is a property of the asset
6
- * *class* (its path prefix), never of the individual file — so a logo and a
7
- * pricing sheet differ only in which class they belong to, and adding a new
8
- * asset type is one entry in {@link ASSET_CLASSES} rather than a new route.
9
- *
10
- * - `public` — anyone, including logged-out visitors. Served with no auth via
11
- * a CDN/public bucket (Supabase) or a shape-gated proxy branch (local). Used
12
- * for branding (logo, favicon) that appears on viewer headers and login pages.
13
- * - `org` — members of the owning org only. The route resolves the orgId
14
- * from the path and runs an org-membership check. Used for org-private docs
15
- * (e.g. pricing sheets) once they exist.
16
- * - `project` — members of the owning project only. The route resolves the
17
- * definition guid → its project and runs the project-view check. Used for
18
- * definition cover images and other per-definition blobs.
19
- */
20
- export type AssetVisibility = 'public' | 'org' | 'project';
21
-
22
- /**
23
- * What scope id the route must authorize against. `none` for public assets
24
- * (no scope to check); `org`/`project` carry the id extracted from the path.
25
- */
26
- export type AssetScope = 'none' | 'org' | 'project';
27
-
28
- export interface AssetClass {
29
- /** Stable identifier — for logging and tests, not user-facing. */
30
- readonly id: string;
31
- readonly visibility: AssetVisibility;
32
- readonly scope: AssetScope;
33
- /**
34
- * Anchored matcher for this class's storage paths. Capture group 1 (when
35
- * present) is the scope id (orgId or guid) the route authorizes against.
36
- */
37
- readonly pattern: RegExp;
38
- }
39
-
40
- export interface AssetMatch {
41
- readonly class: AssetClass;
42
- /** The scope id from the path, or null for `scope: 'none'` classes. */
43
- readonly scopeId: string | null;
44
- }
45
-
46
- // Org branding kinds (logo, favicon, …) are the public set. Built from the
47
- // schema enum so adding a kind there extends this pattern automatically.
48
- const BRANDING_KINDS = ALL_ORG_ASSET_KINDS.join('|');
49
-
50
- // Cover extensions are derived from the canonical `COVER_IMAGE_EXTENSIONS`
51
- // (definitions/types.ts) — strip the leading dot and escape so adding a new
52
- // cover format there propagates here instead of silently 404ing legit covers.
53
- const COVER_EXTS = COVER_IMAGE_EXTENSIONS.map((ext) =>
54
- ext.replace(/^\./, '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
55
- ).join('|');
56
-
57
- // Reusable segment alphabets. `SAFE_SEGMENT` mirrors the `assertSafeKey`
58
- // alphabet in the path helpers (traversal/empty segments are rejected
59
- // separately by `hasUnsafeSegment`); `GUID` is the canonical definition guid
60
- // shape — kept in sync with `UUID_REGEX` in definitions/schemas.ts.
61
- const SAFE_SEGMENT = String.raw`[A-Za-z0-9._-]+`;
62
- const GUID = String.raw`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`;
63
-
64
- /**
65
- * The closed registry of servable asset classes. Order matters only for
66
- * readability — patterns are mutually exclusive by prefix. Anything that
67
- * matches no class is unservable (the route 404s), which keeps the proxy
68
- * default-deny: a new blob type is opt-in by adding an entry here.
69
- */
70
- export const ASSET_CLASSES: readonly AssetClass[] = [
71
- {
72
- id: 'org-branding',
73
- visibility: 'public',
74
- scope: 'none',
75
- pattern: new RegExp(
76
- String.raw`^orgs\/(${SAFE_SEGMENT})\/branding\/(?:${BRANDING_KINDS})\.webp$`
77
- )
78
- },
79
- {
80
- id: 'org-private',
81
- visibility: 'org',
82
- scope: 'org',
83
- pattern: new RegExp(String.raw`^orgs\/(${SAFE_SEGMENT})\/private\/${SAFE_SEGMENT}$`)
84
- },
85
- {
86
- id: 'definition-cover',
87
- visibility: 'project',
88
- scope: 'project',
89
- pattern: new RegExp(String.raw`^definitions\/(${GUID})\/cover\.(?:${COVER_EXTS})$`, 'i')
90
- }
91
- ];
92
-
93
- /**
94
- * Reject any path that contains a traversal, empty, or backslash segment
95
- * *before* it can match a class. The class patterns use a permissive segment
96
- * alphabet (`[A-Za-z0-9._-]+`) so dotted ids and filenames work, but that
97
- * alphabet also admits a bare `.` or `..` *as a whole segment* — which would
98
- * let `orgs/../branding/logo.webp` classify as public branding with `..`
99
- * captured as the orgId. This mirrors `assertSafeKey` in the path builders:
100
- * the registry is the read-side gate, those are the write-side gate, and both
101
- * forbid the same segments so a path that can't be *written* can't be *read*.
102
- */
103
- function hasUnsafeSegment(storagePath: string): boolean {
104
- if (storagePath.includes('\\')) return true;
105
- for (const segment of storagePath.split('/')) {
106
- if (segment === '' || segment === '.' || segment === '..') return true;
107
- }
108
- return false;
109
- }
110
-
111
- /**
112
- * Classify a storage path into its asset class and scope id, or null when no
113
- * class matches (or the path is unsafe). Callers — `getPublicUrl` (to pick CDN
114
- * vs proxy) and the serving route (to pick the auth check) — consult this so
115
- * URL generation and authorization never drift apart.
116
- *
117
- * For `org-branding` the orgId is captured as group 1 but the class is public,
118
- * so `scopeId` is reported as the captured id only when the class actually
119
- * authorizes against it (`scope !== 'none'`); public classes report null.
120
- */
121
- export function classifyAssetPath(storagePath: string): AssetMatch | null {
122
- if (hasUnsafeSegment(storagePath)) return null;
123
- for (const cls of ASSET_CLASSES) {
124
- const m = cls.pattern.exec(storagePath);
125
- if (!m) continue;
126
- return { class: cls, scopeId: cls.scope === 'none' ? null : (m[1] ?? null) };
127
- }
128
- return null;
129
- }
130
-
131
- /** True when the path belongs to a `public` asset class (CDN-servable, no auth). */
132
- export function isPublicAssetPath(storagePath: string): boolean {
133
- return classifyAssetPath(storagePath)?.class.visibility === 'public';
134
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Append a cache-busting token to a stored-asset URL.
3
- *
4
- * Mutable blobs are overwritten **in place** (`IStorageProvider.put` is upsert),
5
- * so a re-upload keeps the same storage path — and therefore the same public
6
- * URL. A browser/CDN that cached the old bytes under that URL keeps serving them
7
- * until the TTL expires, which reads as "the replace didn't work". Appending a
8
- * token that changes only when the content changes gives each new version a
9
- * distinct URL (forcing a refetch) while letting unchanged assets stay cached.
10
- *
11
- * Generic over asset kind: the caller supplies the token from whatever it has
12
- * that tracks the blob's version — a content hash, the owning record's
13
- * `updatedAt`, a version counter. The serving route ignores the query string
14
- * (SvelteKit's `[...path]` param never includes it), so this is purely a
15
- * client-cache key and needs no route support.
16
- *
17
- * Empty/whitespace tokens are dropped (returns the URL unchanged) so a missing
18
- * token degrades to "no cache-bust" rather than `?v=`.
19
- */
20
- export function withCacheBust(url: string, token: string | number | undefined | null): string {
21
- if (token === undefined || token === null) return url;
22
- const value = String(token).trim();
23
- if (value === '') return url;
24
- const sep = url.includes('?') ? '&' : '?';
25
- return `${url}${sep}v=${encodeURIComponent(value)}`;
26
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * Shared image-transcoding helper for `IStorageProvider` implementations.
3
- * Cover images are normalized to WebP at upload time (capped at 1200px,
4
- * quality 85) so storage/bandwidth stays predictable and the viewer path
5
- * is uniform.
6
- *
7
- * `sharp` is loaded via a dynamic import so the platform package has no
8
- * hard runtime dependency on it — install it in providers that need
9
- * transcoding (optional peer dependency).
10
- */
11
-
12
- export const IMAGE_MAX_WIDTH = 1200;
13
- export const IMAGE_WEBP_QUALITY = 85;
14
-
15
- export interface TranscodeResult {
16
- data: Uint8Array;
17
- contentType: string;
18
- /** Possibly-rewritten path (non-webp inputs become `.webp`). */
19
- path: string;
20
- }
21
-
22
- /** True if the input looks like an image to process — by content type or path extension. */
23
- export function isImageUpload(contentType: string | undefined, storagePath: string): boolean {
24
- if (contentType?.startsWith('image/')) return true;
25
- return /\.(webp|png|jpe?g|gif|bmp|tif?f|svg)$/i.test(storagePath);
26
- }
27
-
28
- /**
29
- * Rewrite a path's extension to `.webp`. Unchanged if not a recognized image
30
- * extension. SVG is included: every upload (including SVG) is rasterized to
31
- * WebP, so a `.svg` input is stored as `.webp` — no vector blob is ever
32
- * persisted, which keeps the served bytes XSS-free without a sanitizer.
33
- */
34
- export function toWebpPath(storagePath: string): string {
35
- return storagePath.replace(/\.(png|jpe?g|gif|bmp|tif?f|svg)$/i, '.webp');
36
- }
37
-
38
- /**
39
- * If the input is an image, transcode to WebP (capped + re-encoded) and
40
- * return rewritten bytes/content-type/path. Non-images pass through, so
41
- * providers can call this unconditionally before `put`.
42
- *
43
- * Throws if `sharp` isn't installed — that's a config error, not a fallback.
44
- */
45
- export async function transcodeImageIfNeeded(
46
- data: Uint8Array,
47
- contentType: string | undefined,
48
- storagePath: string
49
- ): Promise<TranscodeResult> {
50
- if (!isImageUpload(contentType, storagePath)) {
51
- return { data, contentType: contentType ?? 'application/octet-stream', path: storagePath };
52
- }
53
-
54
- const sharp = await loadSharp();
55
- const compressed = await sharp(Buffer.from(data))
56
- .resize({ width: IMAGE_MAX_WIDTH, withoutEnlargement: true })
57
- .webp({ quality: IMAGE_WEBP_QUALITY })
58
- .toBuffer();
59
-
60
- return {
61
- data: new Uint8Array(compressed),
62
- contentType: 'image/webp',
63
- path: toWebpPath(storagePath)
64
- };
65
- }
66
-
67
- type SharpFn = typeof import('sharp');
68
- let cachedSharp: SharpFn | null = null;
69
-
70
- async function loadSharp(): Promise<SharpFn> {
71
- if (cachedSharp) return cachedSharp;
72
- try {
73
- const mod = (await import('sharp')) as unknown as { default: SharpFn };
74
- cachedSharp = mod.default ?? (mod as unknown as SharpFn);
75
- return cachedSharp;
76
- } catch (err) {
77
- throw new Error(
78
- 'Image transcoding requires `sharp` to be installed. Add it as a dependency in your provider package.',
79
- { cause: err as Error }
80
- );
81
- }
82
- }
@@ -1,12 +0,0 @@
1
- export type { IStorageProvider } from './interface.js';
2
- export {
3
- IMAGE_MAX_WIDTH,
4
- IMAGE_WEBP_QUALITY,
5
- isImageUpload,
6
- toWebpPath,
7
- transcodeImageIfNeeded
8
- } from './image.js';
9
- export type { TranscodeResult } from './image.js';
10
- export { ASSET_CLASSES, classifyAssetPath, isPublicAssetPath } from './assetClasses.js';
11
- export type { AssetVisibility, AssetScope, AssetClass, AssetMatch } from './assetClasses.js';
12
- export { withCacheBust } from './cacheBust.js';
@@ -1,27 +0,0 @@
1
- /**
2
- * Generic path-based blob storage. Implement with: local filesystem, S3,
3
- * Supabase Storage, Azure Blob, etc.
4
- *
5
- * **Authorization is the caller's responsibility.** Methods do NOT take a
6
- * `RequestContext` — the path itself names the resource, and constructing it
7
- * requires already-authorized scope. Routes/services MUST authorize against
8
- * `@selvajs/platform/access` BEFORE constructing the path.
9
- */
10
- export interface IStorageProvider {
11
- /** Returns null if the path does not exist. */
12
- get(path: string): Promise<Uint8Array | null>;
13
-
14
- put(path: string, data: Uint8Array, contentType?: string): Promise<void>;
15
-
16
- /** No-op if the path does not exist. */
17
- delete(path: string): Promise<void>;
18
-
19
- /** Delete every file whose path starts with `prefix`. */
20
- deletePrefix(prefix: string): Promise<void>;
21
-
22
- /**
23
- * Public URL for a stored file. Local: a relative API path; S3/CDN: a
24
- * full or signed URL.
25
- */
26
- getPublicUrl(path: string): string;
27
- }
@@ -1,47 +0,0 @@
1
- export { runAuthProviderConformance } from './suites/authProviderSuite.js';
2
- export type { AuthProviderConformanceOptions } from './suites/authProviderSuite.js';
3
-
4
- export { runEmailLinkAuthConformance } from './suites/emailLinkAuthSuite.js';
5
- export type { EmailLinkAuthConformanceOptions } from './suites/emailLinkAuthSuite.js';
6
-
7
- export { runDefinitionStoreConformance } from './suites/definitionStoreSuite.js';
8
- export type { DefinitionStoreConformanceOptions } from './suites/definitionStoreSuite.js';
9
-
10
- export { runOrgStoreConformance } from './suites/orgStoreSuite.js';
11
- export type { OrgStoreConformanceOptions } from './suites/orgStoreSuite.js';
12
-
13
- export { runProjectStoreConformance } from './suites/projectStoreSuite.js';
14
- export type { ProjectStoreConformanceOptions } from './suites/projectStoreSuite.js';
15
-
16
- export { runStorageProviderConformance } from './suites/storageProviderSuite.js';
17
- export type { StorageProviderConformanceOptions } from './suites/storageProviderSuite.js';
18
-
19
- export { runUserProfileStoreConformance } from './suites/userProfileStoreSuite.js';
20
- export type { UserProfileStoreConformanceOptions } from './suites/userProfileStoreSuite.js';
21
-
22
- export { runInviteStoreConformance } from './suites/inviteStoreSuite.js';
23
- export type { InviteStoreConformanceOptions, InviteTestScope } from './suites/inviteStoreSuite.js';
24
-
25
- export { runComputeServerStoreConformance } from './suites/computeServerStoreSuite.js';
26
- export type { ComputeServerStoreConformanceOptions } from './suites/computeServerStoreSuite.js';
27
-
28
- export { runShareLinkStoreConformance } from './suites/shareLinkStoreSuite.js';
29
- export type {
30
- ShareLinkStoreConformanceOptions,
31
- ShareLinkTestScope
32
- } from './suites/shareLinkStoreSuite.js';
33
-
34
- export { runEventSinkConformance, RecordingEventSink } from './suites/eventSinkSuite.js';
35
- export type { EventSinkConformanceOptions } from './suites/eventSinkSuite.js';
36
-
37
- export { runSolveMetricSinkConformance } from './suites/solveMetricSinkSuite.js';
38
- export type {
39
- SolveMetricSinkConformanceOptions,
40
- RecordedSolveMetric
41
- } from './suites/solveMetricSinkSuite.js';
42
-
43
- export { runPlatformPermissionStoreConformance } from './suites/platformPermissionStoreSuite.js';
44
- export type { PlatformPermissionStoreConformanceOptions } from './suites/platformPermissionStoreSuite.js';
45
-
46
- export { runPlatformProjectGrantStoreConformance } from './suites/platformProjectGrantStoreSuite.js';
47
- export type { PlatformProjectGrantStoreConformanceOptions } from './suites/platformProjectGrantStoreSuite.js';
@@ -1,207 +0,0 @@
1
- // Conformance suite for IAuthProvider: token round-trips, identity, password auth, admin management.
2
-
3
- import { describe, it, expect } from 'vitest';
4
- import type { IAuthProvider } from '../../auth/index.js';
5
- import { makeUuid } from './helpers.js';
6
-
7
- export interface AuthProviderConformanceOptions {
8
- /** Name to show in test output (e.g. "LocalAuthProvider"). */
9
- name: string;
10
- /**
11
- * Factory returning a fresh provider configured with a known password.
12
- * `adminPassword` must work with `passwordAuth.verifyLogin(adminEmail, adminPassword)`
13
- * for password-capable providers. The factory is responsible for seeding
14
- * the admin user (e.g. via `createUserWithPassword`) before returning.
15
- *
16
- * `adminEmail` defaults to `''` for legacy reasons; new providers should
17
- * always return the seeded admin's email.
18
- */
19
- createProvider: () =>
20
- | Promise<{ provider: IAuthProvider; adminPassword: string; adminEmail?: string }>
21
- | { provider: IAuthProvider; adminPassword: string; adminEmail?: string };
22
- /**
23
- * Set to true when the provider is wired with a backend so that
24
- * createUser / listUsers / updateUserPlatformPermissions / deleteUser tests run.
25
- * When false those methods are expected to return null / 'not_supported'.
26
- */
27
- userManagement?: boolean;
28
- }
29
-
30
- export function runAuthProviderConformance(opts: AuthProviderConformanceOptions): void {
31
- const { name, createProvider, userManagement = false } = opts;
32
-
33
- describe(`IAuthProvider conformance: ${name}`, () => {
34
- // Token round-trips
35
- it('verifyLogin returns a session token that verifyToken accepts', async () => {
36
- const { provider, adminPassword, adminEmail = '' } = await createProvider();
37
- const result = await provider.passwordAuth!.verifyLogin(adminEmail, adminPassword);
38
- expect(result.kind).toBe('success');
39
- if (result.kind !== 'success') return;
40
- const verified = await provider.verifyToken(result.sessionToken);
41
- expect(verified?.id).toBe(result.user.id);
42
- });
43
-
44
- it('verifyToken returns null for a garbage token', async () => {
45
- const { provider } = await createProvider();
46
- const result = await provider.verifyToken('not-a-valid-token');
47
- expect(result).toBeNull();
48
- });
49
-
50
- it('verifyToken returns null for an empty string', async () => {
51
- const { provider } = await createProvider();
52
- const result = await provider.verifyToken('');
53
- expect(result).toBeNull();
54
- });
55
-
56
- // Password auth (only runs when passwordAuth is present)
57
- it('verifyLogin returns success with correct password', async () => {
58
- const { provider, adminPassword, adminEmail = '' } = await createProvider();
59
- if (!provider.passwordAuth) return;
60
- const result = await provider.passwordAuth.verifyLogin(adminEmail, adminPassword);
61
- expect(result.kind).toBe('success');
62
- if (result.kind !== 'success') return;
63
- // Identity-only — platform permissions live on IPlatformPermissionStore now.
64
- expect(result.user.id).toBeTruthy();
65
- expect(typeof result.sessionToken).toBe('string');
66
- expect(result.sessionToken.length).toBeGreaterThan(0);
67
- });
68
-
69
- it('verifyLogin returns failed for wrong password', async () => {
70
- const { provider, adminEmail = '' } = await createProvider();
71
- if (!provider.passwordAuth) return;
72
- const result = await provider.passwordAuth.verifyLogin(adminEmail, 'wrong-password-xyz');
73
- expect(result.kind).toBe('failed');
74
- });
75
-
76
- // getUser
77
- it('getUser returns null for unknown id', async () => {
78
- const { provider } = await createProvider();
79
- // Valid UUID shape but not an actual user — stricter backends (Postgres
80
- // uuid columns) reject non-UUID ids outright, so we never send those.
81
- const result = await provider.getUser(makeUuid());
82
- expect(result).toBeNull();
83
- });
84
-
85
- // ============================================================================
86
- // User management (opt-in)
87
- // ============================================================================
88
-
89
- if (userManagement) {
90
- it('passwordAuth.createUserWithPassword + getUser returns the created user', async () => {
91
- const { provider } = await createProvider();
92
- const email = `test-${makeUuid()}@example.com`;
93
- const created = await provider.passwordAuth!.createUserWithPassword(email, 'password1234');
94
- expect(created).toBeTruthy();
95
- const fetched = await provider.getUser(created.id);
96
- expect(fetched?.id).toBe(created.id);
97
- expect(fetched?.email).toBe(email);
98
- });
99
-
100
- it('createUserWithPassword allows login with the new credentials', async () => {
101
- const { provider } = await createProvider();
102
- const email = `login-${makeUuid()}@example.com`;
103
- await provider.passwordAuth!.createUserWithPassword(email, 'secret9999');
104
- const result = await provider.passwordAuth!.verifyLogin(email, 'secret9999');
105
- expect(result.kind).toBe('success');
106
- if (result.kind !== 'success') return;
107
- expect(result.user.email).toBe(email);
108
- });
109
-
110
- it('listUsers returns created users', async () => {
111
- const { provider } = await createProvider();
112
- const a = `a-${makeUuid()}@example.com`;
113
- const b = `b-${makeUuid()}@example.com`;
114
- await provider.passwordAuth!.createUserWithPassword(a, 'pass12345678');
115
- await provider.passwordAuth!.createUserWithPassword(b, 'pass12345678');
116
- const page = await provider.listUsers({ limit: 200 });
117
- expect(page).toBeTruthy();
118
- const emails = page!.items.map((u) => u.email);
119
- expect(emails).toContain(a);
120
- expect(emails).toContain(b);
121
- });
122
-
123
- it('deleteUser removes the user', async () => {
124
- const { provider } = await createProvider();
125
- const email = `del-${makeUuid()}@example.com`;
126
- const created = await provider.passwordAuth!.createUserWithPassword(email, 'pass12345678');
127
- const result = await provider.deleteUser(created.id);
128
- expect(result).toBe('ok');
129
- const fetched = await provider.getUser(created.id);
130
- expect(fetched).toBeNull();
131
- });
132
-
133
- it('deleteUser returns not_found for unknown user', async () => {
134
- const { provider } = await createProvider();
135
- const result = await provider.deleteUser(makeUuid());
136
- expect(result).toBe('not_found');
137
- });
138
-
139
- // Note: the §2 sole-`instance_admin` invariant is no longer enforced
140
- // by IAuthProvider — it lives on IPlatformPermissionStore now and
141
- // callers consult `countInstanceAdminsExcluding` before destructive
142
- // ops. Auth-side delete/disable just deletes the identity and
143
- // trusts the caller. See `runPlatformPermissionStoreConformance`
144
- // for the invariant tests.
145
-
146
- it('createUserWithPassword returns an identity record without profile fields', async () => {
147
- const { provider } = await createProvider();
148
- const user = await provider.passwordAuth!.createUserWithPassword(
149
- `new-${makeUuid()}@example.com`,
150
- 'pass12345678'
151
- );
152
- // Profile state (starred, recentRuns, displayName) lives on
153
- // IUserProfileStore. Platform permissions live on
154
- // IPlatformPermissionStore. AuthUser is identity-only.
155
- expect(
156
- (user as unknown as { starredDefinitions?: unknown }).starredDefinitions
157
- ).toBeUndefined();
158
- expect((user as unknown as { recentRuns?: unknown }).recentRuns).toBeUndefined();
159
- expect((user as unknown as { displayName?: unknown }).displayName).toBeUndefined();
160
- expect(
161
- (user as unknown as { platformPermissions?: unknown }).platformPermissions
162
- ).toBeUndefined();
163
- });
164
-
165
- it('disableUser disables a user', async () => {
166
- const { provider } = await createProvider();
167
- const created = await provider.passwordAuth!.createUserWithPassword(
168
- `disable-${makeUuid()}@example.com`,
169
- 'pass12345678'
170
- );
171
- const result = await provider.disableUser(created.id);
172
- expect(result).toBe('ok');
173
- const fetched = await provider.getUser(created.id);
174
- expect(fetched?.disabled).toBe(true);
175
- });
176
-
177
- it('disableUser returns not_found for unknown user', async () => {
178
- const { provider } = await createProvider();
179
- const result = await provider.disableUser(makeUuid());
180
- expect(result).toBe('not_found');
181
- });
182
- } else {
183
- it('listUsers returns null when user management is not supported', async () => {
184
- const { provider } = await createProvider();
185
- const result = await provider.listUsers();
186
- expect(result).toBeNull();
187
- });
188
-
189
- it('createUser (allowlist) is undefined when user management is not supported', async () => {
190
- const { provider } = await createProvider();
191
- expect(provider.createUser).toBeUndefined();
192
- });
193
-
194
- it('deleteUser returns not_supported', async () => {
195
- const { provider } = await createProvider();
196
- const result = await provider.deleteUser(makeUuid());
197
- expect(result).toBe('not_supported');
198
- });
199
-
200
- it('disableUser returns not_supported', async () => {
201
- const { provider } = await createProvider();
202
- const result = await provider.disableUser(makeUuid());
203
- expect(result).toBe('not_supported');
204
- });
205
- }
206
- });
207
- }