@selvajs/server 1.2.0 → 1.3.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/dist/access/guards.d.ts +213 -0
- package/dist/access/guards.d.ts.map +1 -0
- package/dist/access/guards.js +417 -0
- package/dist/access/guards.js.map +1 -0
- package/dist/access/index.d.ts +2 -0
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +6 -0
- package/dist/access/index.js.map +1 -1
- package/dist/access/permissions-scope.d.ts +15 -0
- package/dist/access/permissions-scope.d.ts.map +1 -0
- package/dist/access/permissions-scope.js +23 -0
- package/dist/access/permissions-scope.js.map +1 -0
- package/dist/api/bodies.d.ts +104 -0
- package/dist/api/bodies.d.ts.map +1 -0
- package/dist/api/bodies.js +87 -0
- package/dist/api/bodies.js.map +1 -0
- package/dist/api/deps.d.ts +135 -0
- package/dist/api/deps.d.ts.map +1 -0
- package/dist/api/deps.js +55 -0
- package/dist/api/deps.js.map +1 -0
- package/dist/api/errors.d.ts +35 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +63 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +12 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +15 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/map-core-error.d.ts +19 -0
- package/dist/api/map-core-error.d.ts.map +1 -0
- package/dist/api/map-core-error.js +49 -0
- package/dist/api/map-core-error.js.map +1 -0
- package/dist/api/pagination.d.ts +19 -0
- package/dist/api/pagination.d.ts.map +1 -0
- package/dist/api/pagination.js +40 -0
- package/dist/api/pagination.js.map +1 -0
- package/dist/api/request.d.ts +93 -0
- package/dist/api/request.d.ts.map +1 -0
- package/dist/api/request.js +150 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/respond.d.ts +29 -0
- package/dist/api/respond.d.ts.map +1 -0
- package/dist/api/respond.js +59 -0
- package/dist/api/respond.js.map +1 -0
- package/dist/api/responses-schema.d.ts +158 -0
- package/dist/api/responses-schema.d.ts.map +1 -0
- package/dist/api/responses-schema.js +104 -0
- package/dist/api/responses-schema.js.map +1 -0
- package/dist/api/responses.d.ts +19 -0
- package/dist/api/responses.d.ts.map +1 -0
- package/dist/api/responses.js +21 -0
- package/dist/api/responses.js.map +1 -0
- package/dist/api/shaped.d.ts +23 -0
- package/dist/api/shaped.d.ts.map +1 -0
- package/dist/api/shaped.js +29 -0
- package/dist/api/shaped.js.map +1 -0
- package/dist/api/types.d.ts +41 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +12 -0
- package/dist/api/types.js.map +1 -0
- package/dist/compute/errors.d.ts +18 -0
- package/dist/compute/errors.d.ts.map +1 -0
- package/dist/compute/errors.js +21 -0
- package/dist/compute/errors.js.map +1 -0
- package/dist/compute/evict-changed-servers.d.ts +22 -0
- package/dist/compute/evict-changed-servers.d.ts.map +1 -0
- package/dist/compute/evict-changed-servers.js +31 -0
- package/dist/compute/evict-changed-servers.js.map +1 -0
- package/dist/compute/idempotency-http.d.ts +47 -0
- package/dist/compute/idempotency-http.d.ts.map +1 -0
- package/dist/compute/idempotency-http.js +51 -0
- package/dist/compute/idempotency-http.js.map +1 -0
- package/dist/compute/index.d.ts +5 -0
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +5 -0
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/resolve-server.d.ts +25 -0
- package/dist/compute/resolve-server.d.ts.map +1 -0
- package/dist/compute/resolve-server.js +35 -0
- package/dist/compute/resolve-server.js.map +1 -0
- package/dist/compute/server-config-write.d.ts +52 -0
- package/dist/compute/server-config-write.d.ts.map +1 -0
- package/dist/compute/server-config-write.js +100 -0
- package/dist/compute/server-config-write.js.map +1 -0
- package/dist/definitions/index.d.ts +1 -0
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/index.js +1 -0
- package/dist/definitions/index.js.map +1 -1
- package/dist/definitions/visibility.d.ts +74 -0
- package/dist/definitions/visibility.d.ts.map +1 -0
- package/dist/definitions/visibility.js +117 -0
- package/dist/definitions/visibility.js.map +1 -0
- package/dist/handlers/definitionVersions.d.ts +30 -0
- package/dist/handlers/definitionVersions.d.ts.map +1 -0
- package/dist/handlers/definitionVersions.js +93 -0
- package/dist/handlers/definitionVersions.js.map +1 -0
- package/dist/handlers/definitions.d.ts +35 -0
- package/dist/handlers/definitions.d.ts.map +1 -0
- package/dist/handlers/definitions.js +207 -0
- package/dist/handlers/definitions.js.map +1 -0
- package/dist/handlers/index.d.ts +27 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +29 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/invite-lookup.d.ts +11 -0
- package/dist/handlers/invite-lookup.d.ts.map +1 -0
- package/dist/handlers/invite-lookup.js +20 -0
- package/dist/handlers/invite-lookup.js.map +1 -0
- package/dist/handlers/invites.d.ts +31 -0
- package/dist/handlers/invites.d.ts.map +1 -0
- package/dist/handlers/invites.js +148 -0
- package/dist/handlers/invites.js.map +1 -0
- package/dist/handlers/me.d.ts +8 -0
- package/dist/handlers/me.d.ts.map +1 -0
- package/dist/handlers/me.js +21 -0
- package/dist/handlers/me.js.map +1 -0
- package/dist/handlers/me.starred.d.ts +11 -0
- package/dist/handlers/me.starred.d.ts.map +1 -0
- package/dist/handlers/me.starred.js +21 -0
- package/dist/handlers/me.starred.js.map +1 -0
- package/dist/handlers/orgAssets.d.ts +13 -0
- package/dist/handlers/orgAssets.d.ts.map +1 -0
- package/dist/handlers/orgAssets.js +54 -0
- package/dist/handlers/orgAssets.js.map +1 -0
- package/dist/handlers/orgCompute.d.ts +16 -0
- package/dist/handlers/orgCompute.d.ts.map +1 -0
- package/dist/handlers/orgCompute.js +96 -0
- package/dist/handlers/orgCompute.js.map +1 -0
- package/dist/handlers/orgMembers.d.ts +29 -0
- package/dist/handlers/orgMembers.d.ts.map +1 -0
- package/dist/handlers/orgMembers.js +200 -0
- package/dist/handlers/orgMembers.js.map +1 -0
- package/dist/handlers/orgs.d.ts +16 -0
- package/dist/handlers/orgs.d.ts.map +1 -0
- package/dist/handlers/orgs.js +26 -0
- package/dist/handlers/orgs.js.map +1 -0
- package/dist/handlers/projectMembers.d.ts +15 -0
- package/dist/handlers/projectMembers.d.ts.map +1 -0
- package/dist/handlers/projectMembers.js +95 -0
- package/dist/handlers/projectMembers.js.map +1 -0
- package/dist/handlers/projects.d.ts +21 -0
- package/dist/handlers/projects.d.ts.map +1 -0
- package/dist/handlers/projects.js +164 -0
- package/dist/handlers/projects.js.map +1 -0
- package/dist/handlers/reclaim.d.ts +10 -0
- package/dist/handlers/reclaim.d.ts.map +1 -0
- package/dist/handlers/reclaim.js +38 -0
- package/dist/handlers/reclaim.js.map +1 -0
- package/dist/handlers/services.d.ts +22 -0
- package/dist/handlers/services.d.ts.map +1 -0
- package/dist/handlers/services.js +37 -0
- package/dist/handlers/services.js.map +1 -0
- package/dist/handlers/shareLinks.d.ts +23 -0
- package/dist/handlers/shareLinks.d.ts.map +1 -0
- package/dist/handlers/shareLinks.js +85 -0
- package/dist/handlers/shareLinks.js.map +1 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +1 -0
- package/dist/http/index.js.map +1 -1
- package/dist/http/login-rate-limit.d.ts +72 -0
- package/dist/http/login-rate-limit.d.ts.map +1 -0
- package/dist/http/login-rate-limit.js +84 -0
- package/dist/http/login-rate-limit.js.map +1 -0
- package/dist/notifications/deliver-invite.d.ts +27 -0
- package/dist/notifications/deliver-invite.d.ts.map +1 -0
- package/dist/notifications/deliver-invite.js +32 -0
- package/dist/notifications/deliver-invite.js.map +1 -0
- package/dist/notifications/index.d.ts +1 -0
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +1 -0
- package/dist/notifications/index.js.map +1 -1
- package/dist/organizations/index.d.ts +3 -0
- package/dist/organizations/index.d.ts.map +1 -0
- package/dist/organizations/index.js +5 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/organizations/list-all-members.d.ts +10 -0
- package/dist/organizations/list-all-members.d.ts.map +1 -0
- package/dist/organizations/list-all-members.js +36 -0
- package/dist/organizations/list-all-members.js.map +1 -0
- package/dist/organizations/org-asset-service.d.ts +39 -0
- package/dist/organizations/org-asset-service.d.ts.map +1 -0
- package/dist/organizations/org-asset-service.js +71 -0
- package/dist/organizations/org-asset-service.js.map +1 -0
- package/dist/projects/create-project.d.ts +30 -0
- package/dist/projects/create-project.d.ts.map +1 -0
- package/dist/projects/create-project.js +56 -0
- package/dist/projects/create-project.js.map +1 -0
- package/dist/projects/index.d.ts +2 -0
- package/dist/projects/index.d.ts.map +1 -0
- package/dist/projects/index.js +3 -0
- package/dist/projects/index.js.map +1 -0
- package/dist/providers/auth-lookup.d.ts +14 -0
- package/dist/providers/auth-lookup.d.ts.map +1 -0
- package/dist/providers/auth-lookup.js +34 -0
- package/dist/providers/auth-lookup.js.map +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/testing/harness.d.ts +168 -0
- package/dist/testing/harness.d.ts.map +1 -0
- package/dist/testing/harness.js +314 -0
- package/dist/testing/harness.js.map +1 -0
- package/dist/testing/index.d.ts +18 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +15 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/scenarios.d.ts +67 -0
- package/dist/testing/scenarios.d.ts.map +1 -0
- package/dist/testing/scenarios.js +104 -0
- package/dist/testing/scenarios.js.map +1 -0
- package/dist/testing/seed-adapter.d.ts +31 -0
- package/dist/testing/seed-adapter.d.ts.map +1 -0
- package/dist/testing/seed-adapter.js +19 -0
- package/dist/testing/seed-adapter.js.map +1 -0
- package/package.json +30 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IOrgStore, IStorageProvider, OrgAssetKind, RequestContext } from '@selvajs/platform';
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrates org-asset writes that span IStorageProvider + IOrgStore — one
|
|
4
|
+
* service for every asset kind (logo, favicon, …).
|
|
5
|
+
*
|
|
6
|
+
* Every upload is rasterized to WebP by the shared transcoder inside
|
|
7
|
+
* `IStorageProvider.put`, so the stored blob is always
|
|
8
|
+
* `orgs/{id}/branding/{kind}.webp` (the `branding/` segment marks the public
|
|
9
|
+
* asset class — see `classifyAssetPath`). SVG inputs are flattened to a
|
|
10
|
+
* 1200px raster — no vector blob is persisted, which keeps the served bytes
|
|
11
|
+
* XSS-free without a sanitizer. Re-uploads overwrite in place. The org's
|
|
12
|
+
* `assets` map is the source of truth for which kinds exist and their public
|
|
13
|
+
* URLs; we read-merge-write it so concurrent edits to *different* kinds don't
|
|
14
|
+
* clobber each other within a kind.
|
|
15
|
+
*
|
|
16
|
+
* Re-uploads overwrite the same path, so the stored URL would be identical
|
|
17
|
+
* across replaces and a cached browser/CDN would keep serving the old bytes.
|
|
18
|
+
* The content-derived `?v=` token (`withCacheBust`) gives a *different* image a
|
|
19
|
+
* *different* URL, while an identical re-upload keeps its URL and stays cached.
|
|
20
|
+
*/
|
|
21
|
+
export declare class OrgAssetService {
|
|
22
|
+
private orgs;
|
|
23
|
+
private storage;
|
|
24
|
+
constructor(orgs: IOrgStore, storage: IStorageProvider);
|
|
25
|
+
/**
|
|
26
|
+
* Store `kind`'s blob and point the org's `assets[kind]` at its public URL.
|
|
27
|
+
* `contentType` must be the *original* upload type (e.g. `image/svg+xml`) so
|
|
28
|
+
* the transcoder recognizes the bytes as an image. Returns the public URL.
|
|
29
|
+
*/
|
|
30
|
+
saveAsset(ctx: RequestContext, orgId: string, kind: OrgAssetKind, data: Uint8Array, contentType: string): Promise<string>;
|
|
31
|
+
/** Remove `kind`'s blob and drop it from the org's `assets` map. No-op-safe. */
|
|
32
|
+
removeAsset(ctx: RequestContext, orgId: string, kind: OrgAssetKind): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Read the org's current `assets` and return a copy with `kind` set to `url`
|
|
35
|
+
* (or removed when `url` is undefined). Keeps the other kinds intact.
|
|
36
|
+
*/
|
|
37
|
+
private mergedAssets;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=org-asset-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-asset-service.d.ts","sourceRoot":"","sources":["../../src/organizations/org-asset-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnG;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAe;IAE1B,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;gBADP,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,gBAAgB;IAGlC;;;;OAIG;IACG,SAAS,CACd,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAelB,gFAAgF;IAC1E,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF;;;OAGG;YACW,YAAY;CAa1B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { orgPaths, ProviderError, withCacheBust } from '@selvajs/platform';
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrates org-asset writes that span IStorageProvider + IOrgStore — one
|
|
5
|
+
* service for every asset kind (logo, favicon, …).
|
|
6
|
+
*
|
|
7
|
+
* Every upload is rasterized to WebP by the shared transcoder inside
|
|
8
|
+
* `IStorageProvider.put`, so the stored blob is always
|
|
9
|
+
* `orgs/{id}/branding/{kind}.webp` (the `branding/` segment marks the public
|
|
10
|
+
* asset class — see `classifyAssetPath`). SVG inputs are flattened to a
|
|
11
|
+
* 1200px raster — no vector blob is persisted, which keeps the served bytes
|
|
12
|
+
* XSS-free without a sanitizer. Re-uploads overwrite in place. The org's
|
|
13
|
+
* `assets` map is the source of truth for which kinds exist and their public
|
|
14
|
+
* URLs; we read-merge-write it so concurrent edits to *different* kinds don't
|
|
15
|
+
* clobber each other within a kind.
|
|
16
|
+
*
|
|
17
|
+
* Re-uploads overwrite the same path, so the stored URL would be identical
|
|
18
|
+
* across replaces and a cached browser/CDN would keep serving the old bytes.
|
|
19
|
+
* The content-derived `?v=` token (`withCacheBust`) gives a *different* image a
|
|
20
|
+
* *different* URL, while an identical re-upload keeps its URL and stays cached.
|
|
21
|
+
*/
|
|
22
|
+
export class OrgAssetService {
|
|
23
|
+
orgs;
|
|
24
|
+
storage;
|
|
25
|
+
constructor(orgs, storage) {
|
|
26
|
+
this.orgs = orgs;
|
|
27
|
+
this.storage = storage;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Store `kind`'s blob and point the org's `assets[kind]` at its public URL.
|
|
31
|
+
* `contentType` must be the *original* upload type (e.g. `image/svg+xml`) so
|
|
32
|
+
* the transcoder recognizes the bytes as an image. Returns the public URL.
|
|
33
|
+
*/
|
|
34
|
+
async saveAsset(ctx, orgId, kind, data, contentType) {
|
|
35
|
+
const path = orgPaths.asset(orgId, kind);
|
|
36
|
+
await this.storage.put(path, data, contentType);
|
|
37
|
+
// Content-derived token, not a timestamp: an identical re-upload to the
|
|
38
|
+
// same path keeps the same URL and stays cached; a changed image gets a
|
|
39
|
+
// new URL and refetches immediately. 8 hex chars is enough to distinguish
|
|
40
|
+
// versions without bloating the URL.
|
|
41
|
+
const token = createHash('sha256').update(data).digest('hex').slice(0, 8);
|
|
42
|
+
const url = withCacheBust(this.storage.getPublicUrl(path), token);
|
|
43
|
+
await this.orgs.updateOrg(ctx, orgId, {
|
|
44
|
+
assets: await this.mergedAssets(ctx, orgId, kind, url)
|
|
45
|
+
});
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
/** Remove `kind`'s blob and drop it from the org's `assets` map. No-op-safe. */
|
|
49
|
+
async removeAsset(ctx, orgId, kind) {
|
|
50
|
+
await this.storage.delete(orgPaths.asset(orgId, kind));
|
|
51
|
+
await this.orgs.updateOrg(ctx, orgId, {
|
|
52
|
+
assets: await this.mergedAssets(ctx, orgId, kind, undefined)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read the org's current `assets` and return a copy with `kind` set to `url`
|
|
57
|
+
* (or removed when `url` is undefined). Keeps the other kinds intact.
|
|
58
|
+
*/
|
|
59
|
+
async mergedAssets(ctx, orgId, kind, url) {
|
|
60
|
+
const org = await this.orgs.getOrg(ctx, orgId);
|
|
61
|
+
if (!org)
|
|
62
|
+
throw new ProviderError(`Org '${orgId}' not found`, 404);
|
|
63
|
+
const next = { ...(org.assets ?? {}) };
|
|
64
|
+
if (url === undefined)
|
|
65
|
+
delete next[kind];
|
|
66
|
+
else
|
|
67
|
+
next[kind] = url;
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=org-asset-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-asset-service.js","sourceRoot":"","sources":["../../src/organizations/org-asset-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,eAAe;IAElB;IACA;IAFT,YACS,IAAe,EACf,OAAyB;QADzB,SAAI,GAAJ,IAAI,CAAW;QACf,YAAO,GAAP,OAAO,CAAkB;IAC/B,CAAC;IAEJ;;;;OAIG;IACH,KAAK,CAAC,SAAS,CACd,GAAmB,EACnB,KAAa,EACb,IAAkB,EAClB,IAAgB,EAChB,WAAmB;QAEnB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACzC,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAChD,wEAAwE;QACxE,wEAAwE;QACxE,0EAA0E;QAC1E,qCAAqC;QACrC,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;QAClE,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE;YACrC,MAAM,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC;SACtD,CAAC,CAAC;QACH,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,WAAW,CAAC,GAAmB,EAAE,KAAa,EAAE,IAAkB;QACvE,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QACvD,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE;YACrC,MAAM,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC;SAC5D,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,YAAY,CACzB,GAAmB,EACnB,KAAa,EACb,IAAkB,EAClB,GAAuB;QAEvB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,aAAa,CAAC,QAAQ,KAAK,aAAa,EAAE,GAAG,CAAC,CAAC;QACnE,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAA4B,CAAC;QACjE,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;;YACpC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;QACtB,OAAO,IAAI,CAAC;IACb,CAAC;CACD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Project, RequestContext } from '@selvajs/platform';
|
|
2
|
+
/**
|
|
3
|
+
* Fields the caller decides. `slug`, `createdAt`, `updatedAt` and `deletedAt`
|
|
4
|
+
* are derived here so the two create routes cannot disagree about them.
|
|
5
|
+
*/
|
|
6
|
+
export type ProjectDraft = Omit<Project, 'slug' | 'createdAt' | 'updatedAt' | 'deletedAt'>;
|
|
7
|
+
export interface CreateProjectOptions {
|
|
8
|
+
/** Context the store writes under — `SYSTEM_CONTEXT` for the admin route. */
|
|
9
|
+
writeCtx: RequestContext;
|
|
10
|
+
/** Slug stem when the name slugifies to nothing. */
|
|
11
|
+
fallbackSlug: string;
|
|
12
|
+
/** Spliced into the 409 on a duplicate name. */
|
|
13
|
+
conflictScope: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a project, retrying on slug collision.
|
|
17
|
+
*
|
|
18
|
+
* The retry is not a nicety: the caller may not be able to *see* a colliding
|
|
19
|
+
* project under RLS, so a pre-flight `getProjectBySlug` cannot decide it — the
|
|
20
|
+
* unique index is the only source of truth.
|
|
21
|
+
*
|
|
22
|
+
* `/api/v1/projects` and `/api/admin/projects` both create projects and both
|
|
23
|
+
* carried a copy of this loop. The copies had already drifted on three points
|
|
24
|
+
* (error re-throw, slug stem, flag validation), which is what makes this the
|
|
25
|
+
* one place in the access surface where duplication caused real divergence.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createProjectWithUniqueSlug(store: {
|
|
28
|
+
createProject(ctx: RequestContext, project: Project): Promise<unknown>;
|
|
29
|
+
}, draft: ProjectDraft, { writeCtx, fallbackSlug, conflictScope }: CreateProjectOptions): Promise<Project>;
|
|
30
|
+
//# sourceMappingURL=create-project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-project.d.ts","sourceRoot":"","sources":["../../src/projects/create-project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAyBjE;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;AAE3F,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,QAAQ,EAAE,cAAc,CAAC;IACzB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,CAChD,KAAK,EAAE;IACN,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE,EACD,KAAK,EAAE,YAAY,EACnB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,oBAAoB,GAC7D,OAAO,CAAC,OAAO,CAAC,CAmClB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ProviderError, slugify } from '@selvajs/platform';
|
|
2
|
+
import { apiError, ApiErrorCode } from '../api/errors.js';
|
|
3
|
+
const MAX_SLUG_ATTEMPTS = 25;
|
|
4
|
+
// Both predicates match a Postgres constraint name inside an error message.
|
|
5
|
+
// That is fragile enough that it must exist once: a renamed constraint breaks
|
|
6
|
+
// silently — the retry loop stops retrying and a collision surfaces as a 500.
|
|
7
|
+
function isSlugConflict(err) {
|
|
8
|
+
return (err instanceof ProviderError &&
|
|
9
|
+
err.statusCode === 409 &&
|
|
10
|
+
/projects_org_id_slug_key/.test(err.message));
|
|
11
|
+
}
|
|
12
|
+
function isNameConflict(err) {
|
|
13
|
+
return (err instanceof ProviderError &&
|
|
14
|
+
err.statusCode === 409 &&
|
|
15
|
+
/projects_org_name_unique/.test(err.message));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a project, retrying on slug collision.
|
|
19
|
+
*
|
|
20
|
+
* The retry is not a nicety: the caller may not be able to *see* a colliding
|
|
21
|
+
* project under RLS, so a pre-flight `getProjectBySlug` cannot decide it — the
|
|
22
|
+
* unique index is the only source of truth.
|
|
23
|
+
*
|
|
24
|
+
* `/api/v1/projects` and `/api/admin/projects` both create projects and both
|
|
25
|
+
* carried a copy of this loop. The copies had already drifted on three points
|
|
26
|
+
* (error re-throw, slug stem, flag validation), which is what makes this the
|
|
27
|
+
* one place in the access surface where duplication caused real divergence.
|
|
28
|
+
*/
|
|
29
|
+
export async function createProjectWithUniqueSlug(store, draft, { writeCtx, fallbackSlug, conflictScope }) {
|
|
30
|
+
const baseSlug = slugify(draft.name) || fallbackSlug;
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
for (let attempt = 0; attempt < MAX_SLUG_ATTEMPTS; attempt++) {
|
|
33
|
+
const project = {
|
|
34
|
+
...draft,
|
|
35
|
+
slug: attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`,
|
|
36
|
+
createdAt: now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
deletedAt: null
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
await store.createProject(writeCtx, project);
|
|
42
|
+
return project;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (isNameConflict(err)) {
|
|
46
|
+
apiError(409, ApiErrorCode.CONFLICT, `A project with that name already exists in ${conflictScope}.`);
|
|
47
|
+
}
|
|
48
|
+
// A slug clash is the one error worth another attempt; everything else
|
|
49
|
+
// leaves the loop for the route's error wrapper to map.
|
|
50
|
+
if (!isSlugConflict(err))
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
apiError(409, ApiErrorCode.CONFLICT, 'Could not pick a unique project slug after several attempts.');
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=create-project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-project.js","sourceRoot":"","sources":["../../src/projects/create-project.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,4EAA4E;AAC5E,8EAA8E;AAC9E,8EAA8E;AAC9E,SAAS,cAAc,CAAC,GAAY;IACnC,OAAO,CACN,GAAG,YAAY,aAAa;QAC5B,GAAG,CAAC,UAAU,KAAK,GAAG;QACtB,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAC5C,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IACnC,OAAO,CACN,GAAG,YAAY,aAAa;QAC5B,GAAG,CAAC,UAAU,KAAK,GAAG;QACtB,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAC5C,CAAC;AACH,CAAC;AAiBD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAChD,KAEC,EACD,KAAmB,EACnB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAwB;IAE/D,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,iBAAiB,EAAE,OAAO,EAAE,EAAE,CAAC;QAC9D,MAAM,OAAO,GAAY;YACxB,GAAG,KAAK;YACR,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE;YAC7D,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,IAAI;SACf,CAAC;QAEF,IAAI,CAAC;YACJ,MAAM,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC7C,OAAO,OAAO,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,QAAQ,CACP,GAAG,EACH,YAAY,CAAC,QAAQ,EACrB,8CAA8C,aAAa,GAAG,CAC9D,CAAC;YACH,CAAC;YACD,uEAAuE;YACvE,wDAAwD;YACxD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;gBAAE,MAAM,GAAG,CAAC;QACrC,CAAC;IACF,CAAC;IAED,QAAQ,CACP,GAAG,EACH,YAAY,CAAC,QAAQ,EACrB,8DAA8D,CAC9D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/projects/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,2BAA2B,EAC3B,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/projects/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EACN,2BAA2B,EAG3B,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthUser, IAuthProvider } from '@selvajs/platform';
|
|
2
|
+
/**
|
|
3
|
+
* `IAuthProvider` has no by-email lookup and `listUsers` takes no filter, so
|
|
4
|
+
* this scans. Acceptable because the only caller is invite acceptance — once
|
|
5
|
+
* per invite, never on a hot path.
|
|
6
|
+
*
|
|
7
|
+
* A truncated scan would report an existing user as absent and send the flow
|
|
8
|
+
* down the create-account branch, where the provider rejects the duplicate. So
|
|
9
|
+
* exhausting the cursor matters: `null` must mean "not found", not "gave up".
|
|
10
|
+
* Returns `null` when the provider has no user store (`listUsers` → null) or
|
|
11
|
+
* when the cap is hit.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findAuthUserByEmail(auth: IAuthProvider, email: string): Promise<AuthUser | null>;
|
|
14
|
+
//# sourceMappingURL=auth-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-lookup.d.ts","sourceRoot":"","sources":["../../src/providers/auth-lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOjE;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACxC,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAc1B"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const PAGE_LIMIT = 200;
|
|
2
|
+
// Runaway guard against an adapter returning a non-advancing cursor.
|
|
3
|
+
// 100 pages = 20k users, far past any deployment this product targets.
|
|
4
|
+
const MAX_PAGES = 100;
|
|
5
|
+
/**
|
|
6
|
+
* `IAuthProvider` has no by-email lookup and `listUsers` takes no filter, so
|
|
7
|
+
* this scans. Acceptable because the only caller is invite acceptance — once
|
|
8
|
+
* per invite, never on a hot path.
|
|
9
|
+
*
|
|
10
|
+
* A truncated scan would report an existing user as absent and send the flow
|
|
11
|
+
* down the create-account branch, where the provider rejects the duplicate. So
|
|
12
|
+
* exhausting the cursor matters: `null` must mean "not found", not "gave up".
|
|
13
|
+
* Returns `null` when the provider has no user store (`listUsers` → null) or
|
|
14
|
+
* when the cap is hit.
|
|
15
|
+
*/
|
|
16
|
+
export async function findAuthUserByEmail(auth, email) {
|
|
17
|
+
const needle = email.trim().toLowerCase();
|
|
18
|
+
if (!needle)
|
|
19
|
+
return null;
|
|
20
|
+
let cursor;
|
|
21
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
22
|
+
const result = await auth.listUsers({ limit: PAGE_LIMIT, cursor });
|
|
23
|
+
if (!result)
|
|
24
|
+
return null;
|
|
25
|
+
const hit = result.items.find((u) => u.email?.trim().toLowerCase() === needle);
|
|
26
|
+
if (hit)
|
|
27
|
+
return hit;
|
|
28
|
+
cursor = result.nextCursor;
|
|
29
|
+
if (!cursor)
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=auth-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-lookup.js","sourceRoot":"","sources":["../../src/providers/auth-lookup.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,qEAAqE;AACrE,uEAAuE;AACvE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,IAAmB,EACnB,KAAa;IAEb,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,IAAI,MAA0B,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC;QAC/E,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACpB,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;IAC1B,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/providers/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Configurable provider wiring — env-driven selection over a caller-supplied
|
|
2
2
|
// registry, with a `selva.config.js` override hook.
|
|
3
3
|
export { createSelvaProviders } from './create-selva-providers.js';
|
|
4
|
+
export { findAuthUserByEmail } from './auth-lookup.js';
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,oDAAoD;AAEpD,OAAO,EACN,oBAAoB,EAKpB,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,oDAAoD;AAEpD,OAAO,EACN,oBAAoB,EAKpB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host-agnostic harness for testing API handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handlers are transport-free, but the tests around them were not: they built
|
|
5
|
+
* a SvelteKit `RequestEvent` and imported the Selva app's fixtures, so a second
|
|
6
|
+
* host could run the handlers and inherit none of the tests proving they work.
|
|
7
|
+
* That is the duplication this removes — a host wires a `TestHarness` once and
|
|
8
|
+
* gets the shared suites.
|
|
9
|
+
*
|
|
10
|
+
* Two things stay injected because no interface supplies them:
|
|
11
|
+
* - {@link SeedAuthAdapter}, since identity has no portable creation method.
|
|
12
|
+
* - `mapError`, since folding a host's own domain errors into the envelope is
|
|
13
|
+
* by definition host-specific. Omitting it is not neutral: a host whose
|
|
14
|
+
* guards throw something `runHandler` does not recognize gets a 500 where
|
|
15
|
+
* production returns 403, and every status assertion silently weakens.
|
|
16
|
+
*/
|
|
17
|
+
import { emptyProfile, type AuthUser, type DefinitionRecord, type DefinitionVersion, type ILogger, type OrgMember, type OrgPermission, type OrgRole, type Organization, type PlatformPermission, type Project, type ProjectMember, type ProjectRole, type ProjectVisibility, type RequestContext, type SelvaConfig, type ShareLink } from '@selvajs/platform';
|
|
18
|
+
import type { ApiError, ApiHandler, SelvaDeps } from '../api/index.js';
|
|
19
|
+
import type { SeedAuthAdapter } from './seed-adapter.js';
|
|
20
|
+
export interface TestHarness {
|
|
21
|
+
config: SelvaConfig;
|
|
22
|
+
auth: SeedAuthAdapter;
|
|
23
|
+
/** Folds host domain errors into the envelope, exactly as the host's adapter does. */
|
|
24
|
+
mapError?: (err: unknown) => ApiError | undefined;
|
|
25
|
+
/** Overrides merged onto the deps `callHandler` builds — token codecs, services, upload caps. */
|
|
26
|
+
deps?: Partial<Pick<SelvaDeps, 'tokens' | 'uploadLimits' | 'services'>>;
|
|
27
|
+
log?: ILogger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Swallows output unless `LOUD=1`.
|
|
31
|
+
*
|
|
32
|
+
* `runHandler` logs through `req.log` on the 500 path. Without a logger a test
|
|
33
|
+
* that trips an unexpected 500 dies inside the error handler itself, which
|
|
34
|
+
* hides the error that caused it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const silentLog: ILogger;
|
|
37
|
+
export interface SeededUser {
|
|
38
|
+
id: string;
|
|
39
|
+
email: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function seedUser(h: TestHarness, email: string, platformPermissions?: PlatformPermission[]): Promise<SeededUser>;
|
|
42
|
+
export declare function seedOrg(h: TestHarness, opts: {
|
|
43
|
+
name: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
ownerId: string;
|
|
46
|
+
}): Promise<Organization>;
|
|
47
|
+
export declare function seedOrgMember(h: TestHarness, opts: {
|
|
48
|
+
orgId: string;
|
|
49
|
+
userId: string;
|
|
50
|
+
role: OrgRole;
|
|
51
|
+
permissions?: OrgPermission[];
|
|
52
|
+
}): Promise<OrgMember>;
|
|
53
|
+
export declare function seedProject(h: TestHarness, opts: {
|
|
54
|
+
orgId: string;
|
|
55
|
+
name: string;
|
|
56
|
+
slug: string;
|
|
57
|
+
ownerId: string;
|
|
58
|
+
visibility?: ProjectVisibility;
|
|
59
|
+
autoJoinOnUpload?: boolean;
|
|
60
|
+
}): Promise<Project>;
|
|
61
|
+
export declare function seedProjectMember(h: TestHarness, opts: {
|
|
62
|
+
projectId: string;
|
|
63
|
+
userId: string;
|
|
64
|
+
role: ProjectRole;
|
|
65
|
+
}): Promise<ProjectMember>;
|
|
66
|
+
/**
|
|
67
|
+
* Grant platform-wide permissions to a seeded user.
|
|
68
|
+
*
|
|
69
|
+
* Replaces the whole set rather than adding to it, matching
|
|
70
|
+
* `IPermissionStore.set` — a test that means to add has to pass the existing
|
|
71
|
+
* permissions back in.
|
|
72
|
+
*/
|
|
73
|
+
export declare function grantPlatformPermissions(h: TestHarness, userId: string, perms: PlatformPermission[]): Promise<void>;
|
|
74
|
+
export interface SeededDefinition {
|
|
75
|
+
record: DefinitionRecord;
|
|
76
|
+
version: DefinitionVersion;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A definition with one version, written through the host's `DefinitionService`
|
|
80
|
+
* rather than straight into the store.
|
|
81
|
+
*
|
|
82
|
+
* Going through the service is what makes the fixture match production: it is
|
|
83
|
+
* what allocates the version, writes the blob, and moves the channel pointer,
|
|
84
|
+
* and a test that seeded the record alone would exercise a definition no solve
|
|
85
|
+
* path can load.
|
|
86
|
+
*/
|
|
87
|
+
export declare function seedDefinition(h: TestHarness, opts: {
|
|
88
|
+
projectId: string;
|
|
89
|
+
ownerId: string;
|
|
90
|
+
displayName?: string;
|
|
91
|
+
guid?: string;
|
|
92
|
+
}): Promise<SeededDefinition>;
|
|
93
|
+
export interface SeededShareLink {
|
|
94
|
+
link: ShareLink;
|
|
95
|
+
rawToken: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A share link plus the plaintext token behind it.
|
|
99
|
+
*
|
|
100
|
+
* Minted through the harness's own codec, so the stored hash verifies against
|
|
101
|
+
* the same secret a handler resolves from `deps.tokens.shareLinks` — a second
|
|
102
|
+
* codec would hash to something no handler could ever match.
|
|
103
|
+
*/
|
|
104
|
+
export declare function seedShareLink(h: TestHarness, opts: {
|
|
105
|
+
definitionId: string;
|
|
106
|
+
channel?: 'live' | 'draft';
|
|
107
|
+
createdBy: string;
|
|
108
|
+
allowSolve?: boolean;
|
|
109
|
+
maxSolves?: number | null;
|
|
110
|
+
expiresAt?: string | null;
|
|
111
|
+
}): Promise<SeededShareLink>;
|
|
112
|
+
/**
|
|
113
|
+
* The harness a `callHandler` call should build its deps from, carried on the
|
|
114
|
+
* locals rather than passed alongside them.
|
|
115
|
+
*
|
|
116
|
+
* A test already threads `locals` from `actAs` into every call; making the
|
|
117
|
+
* harness a second argument would mean passing the same two values everywhere
|
|
118
|
+
* and would let them disagree — locals from one stack, deps from another, which
|
|
119
|
+
* type-checks and silently tests nothing.
|
|
120
|
+
*/
|
|
121
|
+
declare const HARNESS: unique symbol;
|
|
122
|
+
export interface ActingLocals {
|
|
123
|
+
user: AuthUser;
|
|
124
|
+
ctx: RequestContext;
|
|
125
|
+
profile: ReturnType<typeof emptyProfile>;
|
|
126
|
+
providers: SelvaConfig;
|
|
127
|
+
log: ILogger;
|
|
128
|
+
[HARNESS]?: TestHarness;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build the per-request identity a handler sees.
|
|
132
|
+
*
|
|
133
|
+
* Mirrors what a host's auth hook produces: one membership lookup, plus the
|
|
134
|
+
* instance-admin fallback to the first listed org for an admin who belongs to
|
|
135
|
+
* none. Keeping this aligned with the host is the point of testing against real
|
|
136
|
+
* stores rather than mocks — a fixture that invented its own `ctx` would prove
|
|
137
|
+
* the handler works under scope no request ever has.
|
|
138
|
+
*/
|
|
139
|
+
export declare function actAs(h: TestHarness, userId: string): Promise<ActingLocals>;
|
|
140
|
+
export declare function anon(h: TestHarness): {
|
|
141
|
+
providers: SelvaConfig;
|
|
142
|
+
[HARNESS]?: TestHarness;
|
|
143
|
+
};
|
|
144
|
+
export interface CallHandlerOpts {
|
|
145
|
+
locals: unknown;
|
|
146
|
+
params?: Record<string, string>;
|
|
147
|
+
url?: string;
|
|
148
|
+
body?: unknown;
|
|
149
|
+
headers?: Record<string, string>;
|
|
150
|
+
}
|
|
151
|
+
export interface CallResult {
|
|
152
|
+
status: number;
|
|
153
|
+
headers: Headers;
|
|
154
|
+
json?: unknown;
|
|
155
|
+
text?: string;
|
|
156
|
+
location?: string;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Invoke an `ApiHandler` with no host framework in the loop.
|
|
160
|
+
*
|
|
161
|
+
* Still goes through `runHandler`, because that is the contract: it is what
|
|
162
|
+
* turns a thrown `ApiError` — or a host error `mapError` recognizes — into a
|
|
163
|
+
* status. Calling the handler bare would make every failure an exception and
|
|
164
|
+
* every status assertion untestable.
|
|
165
|
+
*/
|
|
166
|
+
export declare function callHandler(handler: ApiHandler, opts: CallHandlerOpts): Promise<CallResult>;
|
|
167
|
+
export {};
|
|
168
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/testing/harness.ts"],"names":[],"mappings":"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"}
|