@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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org owner/admin escape hatch for a project they cannot otherwise reach.
|
|
3
|
+
*
|
|
4
|
+
* Adds the actor as a co-owner and does NOT demote the existing owner.
|
|
5
|
+
* Idempotent: reclaiming twice is a no-op, because `addProjectMember`
|
|
6
|
+
* reactivates a soft-deleted row in place.
|
|
7
|
+
*/
|
|
8
|
+
import { created, requireParams } from '../api/index.js';
|
|
9
|
+
import { actorFrom } from '@selvajs/platform';
|
|
10
|
+
import { requireCanReclaim } from '../access/index.js';
|
|
11
|
+
export const reclaimProject = async (req) => {
|
|
12
|
+
const { id } = requireParams(req.params, 'id');
|
|
13
|
+
const { user, ctx, project } = await requireCanReclaim(req, id);
|
|
14
|
+
const now = new Date().toISOString();
|
|
15
|
+
const member = {
|
|
16
|
+
projectId: id,
|
|
17
|
+
userId: user.id,
|
|
18
|
+
role: 'owner',
|
|
19
|
+
joinedAt: now,
|
|
20
|
+
updatedAt: now,
|
|
21
|
+
updatedBy: ctx.userId || user.id,
|
|
22
|
+
deletedAt: null
|
|
23
|
+
};
|
|
24
|
+
await req.deps.projects.addProjectMember(ctx, member);
|
|
25
|
+
// `addProjectMember` emits `project_member.added`, which is byte-identical
|
|
26
|
+
// to an owner adding a teammate. The whole escape hatch rests on the audit
|
|
27
|
+
// trail, so the escalation needs its own event or the log cannot answer the
|
|
28
|
+
// only question an auditor has here.
|
|
29
|
+
await req.deps.events.emit({
|
|
30
|
+
type: 'project.reclaimed',
|
|
31
|
+
projectId: id,
|
|
32
|
+
orgId: project.orgId,
|
|
33
|
+
actorId: actorFrom(ctx),
|
|
34
|
+
priorVisibility: project.visibility
|
|
35
|
+
});
|
|
36
|
+
return created({ member });
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=reclaim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reclaim.js","sourceRoot":"","sources":["../../src/handlers/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAmB,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAsB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,CAAC,MAAM,cAAc,GAAe,KAAK,EAAE,GAAG,EAAE,EAAE;IACvD,MAAM,EAAE,EAAE,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAEhE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,MAAM,GAAkB;QAC7B,SAAS,EAAE,EAAE;QACb,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE,GAAG;QACb,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE;QAChC,SAAS,EAAE,IAAI;KACf,CAAC;IAEF,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEtD,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,qCAAqC;IACrC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;QAC1B,IAAI,EAAE,mBAAmB;QACzB,SAAS,EAAE,EAAE;QACb,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC,UAAU;KACnC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessors for the host-composed services on `SelvaDeps`.
|
|
3
|
+
*
|
|
4
|
+
* `services` is host-supplied and optional by type, so every call site would
|
|
5
|
+
* otherwise repeat a presence check or reach for `!`. A host that mounts these
|
|
6
|
+
* handlers without wiring a service it needs gets a named error here rather
|
|
7
|
+
* than `Cannot read properties of undefined` from inside the service call.
|
|
8
|
+
*/
|
|
9
|
+
import type { SelvaDeps } from '../api/index.js';
|
|
10
|
+
import type { TokenCodec } from '../tokens/index.js';
|
|
11
|
+
import type { OrgAssetService } from '../organizations/index.js';
|
|
12
|
+
export declare function definitionService(deps: SelvaDeps): import("../definitions/definition-service.js").DefinitionService;
|
|
13
|
+
export declare function orgAssetService(deps: SelvaDeps): OrgAssetService;
|
|
14
|
+
/**
|
|
15
|
+
* A token codec, or a named failure.
|
|
16
|
+
*
|
|
17
|
+
* Minting through an absent codec is the one failure mode worth being loud
|
|
18
|
+
* about: a handler that quietly skipped hashing would persist a row whose token
|
|
19
|
+
* can never be verified, and nothing downstream would report it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function tokenCodec(deps: SelvaDeps, family: keyof SelvaDeps['tokens']): TokenCodec;
|
|
22
|
+
//# sourceMappingURL=services.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/handlers/services.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,oEAMhD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAMhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,GAAG,UAAU,CAMzF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessors for the host-composed services on `SelvaDeps`.
|
|
3
|
+
*
|
|
4
|
+
* `services` is host-supplied and optional by type, so every call site would
|
|
5
|
+
* otherwise repeat a presence check or reach for `!`. A host that mounts these
|
|
6
|
+
* handlers without wiring a service it needs gets a named error here rather
|
|
7
|
+
* than `Cannot read properties of undefined` from inside the service call.
|
|
8
|
+
*/
|
|
9
|
+
export function definitionService(deps) {
|
|
10
|
+
const service = deps.services.definitions;
|
|
11
|
+
if (!service) {
|
|
12
|
+
throw new Error('SelvaDeps.services.definitions is not wired — mount requires it.');
|
|
13
|
+
}
|
|
14
|
+
return service;
|
|
15
|
+
}
|
|
16
|
+
export function orgAssetService(deps) {
|
|
17
|
+
const service = deps.services.orgAssets;
|
|
18
|
+
if (!service) {
|
|
19
|
+
throw new Error('SelvaDeps.services.orgAssets is not wired — mount requires it.');
|
|
20
|
+
}
|
|
21
|
+
return service;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A token codec, or a named failure.
|
|
25
|
+
*
|
|
26
|
+
* Minting through an absent codec is the one failure mode worth being loud
|
|
27
|
+
* about: a handler that quietly skipped hashing would persist a row whose token
|
|
28
|
+
* can never be verified, and nothing downstream would report it.
|
|
29
|
+
*/
|
|
30
|
+
export function tokenCodec(deps, family) {
|
|
31
|
+
const codec = deps.tokens[family];
|
|
32
|
+
if (!codec) {
|
|
33
|
+
throw new Error(`SelvaDeps.tokens.${family} is not wired — mount requires it.`);
|
|
34
|
+
}
|
|
35
|
+
return codec;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=services.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services.js","sourceRoot":"","sources":["../../src/handlers/services.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,UAAU,iBAAiB,CAAC,IAAe;IAChD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAe;IAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAe,EAAE,MAAiC;IAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,oCAAoC,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share-link administration.
|
|
3
|
+
*
|
|
4
|
+
* Minting, listing and revoking are gated by `canEditDefinition` — the same
|
|
5
|
+
* authority that uploads versions and publishes. The raw token appears in the
|
|
6
|
+
* POST response and nowhere else; everything afterwards sees `hasToken: true`.
|
|
7
|
+
*
|
|
8
|
+
* **Responses go through `shaped`/`shapedCollection`, never `{ body }`.** A
|
|
9
|
+
* `ShareLink` carries `tokenHash`, and the response schema is what drops it —
|
|
10
|
+
* returning the record directly would put a credential hash on the wire with
|
|
11
|
+
* nothing failing at build time.
|
|
12
|
+
*/
|
|
13
|
+
import type { ApiHandler } from '../api/index.js';
|
|
14
|
+
export declare const listShareLinks: ApiHandler;
|
|
15
|
+
export declare const createShareLink: ApiHandler;
|
|
16
|
+
/**
|
|
17
|
+
* Revoke a share link (soft-delete). Idempotent, gated by `canEditDefinition`.
|
|
18
|
+
*
|
|
19
|
+
* The link must belong to this definition, so one definition's editor cannot
|
|
20
|
+
* revoke another's link with a guessed or leaked linkId.
|
|
21
|
+
*/
|
|
22
|
+
export declare const revokeShareLink: ApiHandler;
|
|
23
|
+
//# sourceMappingURL=shareLinks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shareLinks.d.ts","sourceRoot":"","sources":["../../src/handlers/shareLinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAgBH,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,iBAAiB,CAAC;AAsB9D,eAAO,MAAM,cAAc,EAAE,UAU5B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,UA8B7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,UAc7B,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share-link administration.
|
|
3
|
+
*
|
|
4
|
+
* Minting, listing and revoking are gated by `canEditDefinition` — the same
|
|
5
|
+
* authority that uploads versions and publishes. The raw token appears in the
|
|
6
|
+
* POST response and nowhere else; everything afterwards sees `hasToken: true`.
|
|
7
|
+
*
|
|
8
|
+
* **Responses go through `shaped`/`shapedCollection`, never `{ body }`.** A
|
|
9
|
+
* `ShareLink` carries `tokenHash`, and the response schema is what drops it —
|
|
10
|
+
* returning the record directly would put a credential hash on the wire with
|
|
11
|
+
* nothing failing at build time.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { apiError, ApiErrorCode, CreatedShareLinkResponseSchema, noContent, parseBody, parseListOptions, parseParam, requireCaller, shaped, shapedCollection, ShareLinkResponseSchema } from '../api/index.js';
|
|
15
|
+
import { CreateShareLinkInputSchema, DEFAULT_SHARE_LINK_MAX_SOLVES } from '@selvajs/platform';
|
|
16
|
+
import { GuidSchema } from '@selvajs/platform/definitions';
|
|
17
|
+
import { requireEditableDefinition } from '../access/index.js';
|
|
18
|
+
import { tokenCodec } from './services.js';
|
|
19
|
+
function assertSharingEnabled(req) {
|
|
20
|
+
if (!req.deps.flag('ENABLE_SHARING')) {
|
|
21
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Share links are disabled on this instance (ENABLE_SHARING).');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** `tokenHash` is dropped by the response schema, not by this function. */
|
|
25
|
+
function forClient(link) {
|
|
26
|
+
return { ...link, hasToken: true };
|
|
27
|
+
}
|
|
28
|
+
export const listShareLinks = async (req) => {
|
|
29
|
+
assertSharingEnabled(req);
|
|
30
|
+
const guid = parseParam(req.params.guid, GuidSchema, 'GUID');
|
|
31
|
+
const { ctx } = await requireEditableDefinition(req, guid);
|
|
32
|
+
const page = await req.deps.shareLinks.listByDefinition(ctx, guid, parseListOptions(req.url));
|
|
33
|
+
return shapedCollection(ShareLinkResponseSchema, {
|
|
34
|
+
items: page.items.map(forClient),
|
|
35
|
+
nextCursor: page.nextCursor
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
export const createShareLink = async (req) => {
|
|
39
|
+
assertSharingEnabled(req);
|
|
40
|
+
const guid = parseParam(req.params.guid, GuidSchema, 'GUID');
|
|
41
|
+
const { user } = requireCaller(req);
|
|
42
|
+
const { ctx } = await requireEditableDefinition(req, guid);
|
|
43
|
+
const input = await parseBody(req.request, CreateShareLinkInputSchema, { missingAs: {} });
|
|
44
|
+
const codec = tokenCodec(req.deps, 'shareLinks');
|
|
45
|
+
const raw = codec.mintRawToken();
|
|
46
|
+
const link = {
|
|
47
|
+
id: randomUUID(),
|
|
48
|
+
definitionId: guid,
|
|
49
|
+
channel: input.channel,
|
|
50
|
+
tokenHash: codec.hashToken(raw),
|
|
51
|
+
name: input.name,
|
|
52
|
+
createdBy: user.id,
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
expiresAt: input.expiresAt ?? null,
|
|
55
|
+
revokedAt: null,
|
|
56
|
+
allowSolve: input.allowSolve,
|
|
57
|
+
// The default applies only when the field is absent. An explicit `null`
|
|
58
|
+
// is a deliberate "uncap" choice and is preserved.
|
|
59
|
+
maxSolves: input.maxSolves === undefined ? DEFAULT_SHARE_LINK_MAX_SOLVES : input.maxSolves,
|
|
60
|
+
solveCount: 0
|
|
61
|
+
};
|
|
62
|
+
await req.deps.shareLinks.create(ctx, link);
|
|
63
|
+
// The token is returned once — a client that loses it must revoke and re-mint.
|
|
64
|
+
return shaped(CreatedShareLinkResponseSchema, { link: forClient(link), token: raw }, 201);
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Revoke a share link (soft-delete). Idempotent, gated by `canEditDefinition`.
|
|
68
|
+
*
|
|
69
|
+
* The link must belong to this definition, so one definition's editor cannot
|
|
70
|
+
* revoke another's link with a guessed or leaked linkId.
|
|
71
|
+
*/
|
|
72
|
+
export const revokeShareLink = async (req) => {
|
|
73
|
+
assertSharingEnabled(req);
|
|
74
|
+
const guid = parseParam(req.params.guid, GuidSchema, 'GUID');
|
|
75
|
+
const linkId = parseParam(req.params.linkId, GuidSchema, 'link ID');
|
|
76
|
+
const { ctx } = await requireEditableDefinition(req, guid);
|
|
77
|
+
const existing = await req.deps.shareLinks.getById(ctx, linkId);
|
|
78
|
+
if (!existing || existing.definitionId !== guid) {
|
|
79
|
+
// 404 either way — never disclose that the id exists on another definition.
|
|
80
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Share link not found.');
|
|
81
|
+
}
|
|
82
|
+
await req.deps.shareLinks.revoke(ctx, linkId);
|
|
83
|
+
return noContent();
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=shareLinks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shareLinks.js","sourceRoot":"","sources":["../../src/handlers/shareLinks.ts"],"names":[],"mappings":"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"}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { safeRedirectTarget } from './redirect.js';
|
|
|
2
2
|
export { declaredBodySizeExceeds, type HeadersLike } from './body-size.js';
|
|
3
3
|
export { applySecurityHeaders, type SecurityHeaderOptions } from './security-headers.js';
|
|
4
4
|
export { createRouteClassifier, type RouteClassifier, type RouteClassifierConfig } from './route-classifier.js';
|
|
5
|
+
export { createLoginRateLimiter, addressKeysCollapsed, type LoginRateLimiter, type LoginRateLimiterConfig } from './login-rate-limit.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC"}
|
package/dist/http/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export { safeRedirectTarget } from './redirect.js';
|
|
|
3
3
|
export { declaredBodySizeExceeds } from './body-size.js';
|
|
4
4
|
export { applySecurityHeaders } from './security-headers.js';
|
|
5
5
|
export { createRouteClassifier } from './route-classifier.js';
|
|
6
|
+
export { createLoginRateLimiter, addressKeysCollapsed } from './login-rate-limit.js';
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
package/dist/http/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EAGpB,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login rate limiting: two buckets per attempt, because neither dimension
|
|
3
|
+
* covers the other.
|
|
4
|
+
*
|
|
5
|
+
* - **Per-address** bounds one client hammering the form. It is only
|
|
6
|
+
* meaningful when the host resolves a real client address. Behind a reverse
|
|
7
|
+
* proxy that is not configured to forward one, every request reports the
|
|
8
|
+
* socket peer — `127.0.0.1` for all of them — which collapses the whole key
|
|
9
|
+
* space into a single bucket: five failed logins from anywhere lock out the
|
|
10
|
+
* entire instance, and only a success clears it, which nobody can now
|
|
11
|
+
* reach. That failure is completely silent, hence
|
|
12
|
+
* {@link addressKeysCollapsed}.
|
|
13
|
+
* - **Per-account** bounds a targeted guessing attack, which address limiting
|
|
14
|
+
* does not: an attacker spread across many source IPs contends with no
|
|
15
|
+
* shared counter at all, leaving password-hash cost as the only real bound
|
|
16
|
+
* on online guessing.
|
|
17
|
+
*
|
|
18
|
+
* An attempt must clear both and a failure charges both. A success clears the
|
|
19
|
+
* account bucket and the calling address's — a *different* address that failed
|
|
20
|
+
* against the same account keeps its own penalty.
|
|
21
|
+
*
|
|
22
|
+
* The flow counts failures rather than attempts: `check` gates without spending
|
|
23
|
+
* budget, `recordFailure` spends, and `clear` forgives. A successful login
|
|
24
|
+
* therefore costs nothing, so an ordinary user typing one wrong password never
|
|
25
|
+
* walks toward a lockout.
|
|
26
|
+
*/
|
|
27
|
+
import type { RateLimitResult } from '../compute/rate-limit.js';
|
|
28
|
+
export interface LoginRateLimiterConfig {
|
|
29
|
+
/** Window for both buckets. Default 15 minutes. */
|
|
30
|
+
windowMs?: number;
|
|
31
|
+
/** Failed logins per address per window. Default 5. */
|
|
32
|
+
maxPerAddress?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Failed logins per account per window. Default 20 — deliberately looser
|
|
35
|
+
* than the address cap, because this bucket is reachable by an attacker who
|
|
36
|
+
* wants to lock a known user out of their own account. It trades a little
|
|
37
|
+
* brute-force headroom for not being a cheap denial of service on a named
|
|
38
|
+
* victim.
|
|
39
|
+
*/
|
|
40
|
+
maxPerAccount?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface LoginRateLimiter {
|
|
43
|
+
/**
|
|
44
|
+
* Gate one attempt without spending budget.
|
|
45
|
+
*
|
|
46
|
+
* `email` may be absent — a password-only login against a provider with no
|
|
47
|
+
* user store has no account to key on — in which case only the address
|
|
48
|
+
* bucket applies.
|
|
49
|
+
*/
|
|
50
|
+
check(address: string, email?: string): RateLimitResult;
|
|
51
|
+
/** Charge a failed attempt to both buckets. */
|
|
52
|
+
recordFailure(address: string, email?: string): void;
|
|
53
|
+
/** Forgive on success. */
|
|
54
|
+
clear(address: string, email?: string): void;
|
|
55
|
+
/** Drop all state. For tests. */
|
|
56
|
+
reset(): void;
|
|
57
|
+
}
|
|
58
|
+
export declare function createLoginRateLimiter(config?: LoginRateLimiterConfig): LoginRateLimiter;
|
|
59
|
+
/**
|
|
60
|
+
* Whether per-address limiting has degenerated into one shared bucket.
|
|
61
|
+
*
|
|
62
|
+
* True when the host reports a loopback address for a request it did not
|
|
63
|
+
* originate — which means it sits behind a proxy and is reading the socket peer
|
|
64
|
+
* instead of a forwarded client address. The host decides what to do about it;
|
|
65
|
+
* warning once at startup is enough, and warning per request is not.
|
|
66
|
+
*
|
|
67
|
+
* `addressHeaderConfigured` is the host's answer to "am I reading a forwarded
|
|
68
|
+
* header at all", since which header and how many hops to trust are deployment
|
|
69
|
+
* config that no package can resolve for it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function addressKeysCollapsed(address: string, addressHeaderConfigured: boolean): boolean;
|
|
72
|
+
//# sourceMappingURL=login-rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/login-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKhE,MAAM,WAAW,sBAAsB;IACtC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IACxD,+CAA+C;IAC/C,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrD,0BAA0B;IAC1B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,iCAAiC;IACjC,KAAK,IAAI,IAAI,CAAC;CACd;AAOD,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,sBAA2B,GAAG,gBAAgB,CA8B5F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAE,OAAO,GAAG,OAAO,CAG/F"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login rate limiting: two buckets per attempt, because neither dimension
|
|
3
|
+
* covers the other.
|
|
4
|
+
*
|
|
5
|
+
* - **Per-address** bounds one client hammering the form. It is only
|
|
6
|
+
* meaningful when the host resolves a real client address. Behind a reverse
|
|
7
|
+
* proxy that is not configured to forward one, every request reports the
|
|
8
|
+
* socket peer — `127.0.0.1` for all of them — which collapses the whole key
|
|
9
|
+
* space into a single bucket: five failed logins from anywhere lock out the
|
|
10
|
+
* entire instance, and only a success clears it, which nobody can now
|
|
11
|
+
* reach. That failure is completely silent, hence
|
|
12
|
+
* {@link addressKeysCollapsed}.
|
|
13
|
+
* - **Per-account** bounds a targeted guessing attack, which address limiting
|
|
14
|
+
* does not: an attacker spread across many source IPs contends with no
|
|
15
|
+
* shared counter at all, leaving password-hash cost as the only real bound
|
|
16
|
+
* on online guessing.
|
|
17
|
+
*
|
|
18
|
+
* An attempt must clear both and a failure charges both. A success clears the
|
|
19
|
+
* account bucket and the calling address's — a *different* address that failed
|
|
20
|
+
* against the same account keeps its own penalty.
|
|
21
|
+
*
|
|
22
|
+
* The flow counts failures rather than attempts: `check` gates without spending
|
|
23
|
+
* budget, `recordFailure` spends, and `clear` forgives. A successful login
|
|
24
|
+
* therefore costs nothing, so an ordinary user typing one wrong password never
|
|
25
|
+
* walks toward a lockout.
|
|
26
|
+
*/
|
|
27
|
+
import { createComputeRateLimiter } from '../compute/rate-limit.js';
|
|
28
|
+
/** Loopback in the three spellings a Node server actually reports. */
|
|
29
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
30
|
+
/** Normalized so case tricks (`Alice@` vs `alice@`) share one bucket. */
|
|
31
|
+
function accountKey(email) {
|
|
32
|
+
return `account:${email.trim().toLowerCase()}`;
|
|
33
|
+
}
|
|
34
|
+
export function createLoginRateLimiter(config = {}) {
|
|
35
|
+
const windowMs = config.windowMs ?? 15 * 60 * 1000;
|
|
36
|
+
const byAddress = createComputeRateLimiter({
|
|
37
|
+
windowMs,
|
|
38
|
+
maxPerWindow: config.maxPerAddress ?? 5
|
|
39
|
+
});
|
|
40
|
+
const byAccount = createComputeRateLimiter({
|
|
41
|
+
windowMs,
|
|
42
|
+
maxPerWindow: config.maxPerAccount ?? 20
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
check(address, email) {
|
|
46
|
+
const address_ = byAddress.peek(address);
|
|
47
|
+
if (!address_.allowed || !email)
|
|
48
|
+
return address_;
|
|
49
|
+
return byAccount.peek(accountKey(email));
|
|
50
|
+
},
|
|
51
|
+
recordFailure(address, email) {
|
|
52
|
+
byAddress.check(address);
|
|
53
|
+
if (email)
|
|
54
|
+
byAccount.check(accountKey(email));
|
|
55
|
+
},
|
|
56
|
+
clear(address, email) {
|
|
57
|
+
byAddress.clear(address);
|
|
58
|
+
if (email)
|
|
59
|
+
byAccount.clear(accountKey(email));
|
|
60
|
+
},
|
|
61
|
+
reset() {
|
|
62
|
+
byAddress.reset();
|
|
63
|
+
byAccount.reset();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether per-address limiting has degenerated into one shared bucket.
|
|
69
|
+
*
|
|
70
|
+
* True when the host reports a loopback address for a request it did not
|
|
71
|
+
* originate — which means it sits behind a proxy and is reading the socket peer
|
|
72
|
+
* instead of a forwarded client address. The host decides what to do about it;
|
|
73
|
+
* warning once at startup is enough, and warning per request is not.
|
|
74
|
+
*
|
|
75
|
+
* `addressHeaderConfigured` is the host's answer to "am I reading a forwarded
|
|
76
|
+
* header at all", since which header and how many hops to trust are deployment
|
|
77
|
+
* config that no package can resolve for it.
|
|
78
|
+
*/
|
|
79
|
+
export function addressKeysCollapsed(address, addressHeaderConfigured) {
|
|
80
|
+
if (addressHeaderConfigured)
|
|
81
|
+
return false;
|
|
82
|
+
return LOOPBACK.has(address);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=login-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-rate-limit.js","sourceRoot":"","sources":["../../src/http/login-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,sEAAsE;AACtE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAkCnE,yEAAyE;AACzE,SAAS,UAAU,CAAC,KAAa;IAChC,OAAO,WAAW,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,SAAiC,EAAE;IACzE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACnD,MAAM,SAAS,GAAG,wBAAwB,CAAC;QAC1C,QAAQ;QACR,YAAY,EAAE,MAAM,CAAC,aAAa,IAAI,CAAC;KACvC,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,wBAAwB,CAAC;QAC1C,QAAQ;QACR,YAAY,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;KACxC,CAAC,CAAC;IAEH,OAAO;QACN,KAAK,CAAC,OAAO,EAAE,KAAK;YACnB,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzC,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,KAAK;gBAAE,OAAO,QAAQ,CAAC;YACjD,OAAO,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,aAAa,CAAC,OAAO,EAAE,KAAK;YAC3B,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,IAAI,KAAK;gBAAE,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,OAAO,EAAE,KAAK;YACnB,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,IAAI,KAAK;gBAAE,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK;YACJ,SAAS,CAAC,KAAK,EAAE,CAAC;YAClB,SAAS,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;KACD,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,uBAAgC;IACrF,IAAI,uBAAuB;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuthUser, IDataProvider, ILogger, INotificationProvider, Invite, RequestContext, UserProfile } from '@selvajs/platform';
|
|
2
|
+
export type InviteDelivery = 'sent' | 'not-configured' | 'failed';
|
|
3
|
+
export interface DeliverInviteInput {
|
|
4
|
+
ctx: RequestContext;
|
|
5
|
+
log: ILogger;
|
|
6
|
+
invite: Invite;
|
|
7
|
+
acceptUrl: string;
|
|
8
|
+
/** The admin minting the invite — used only for the "X invited you" line. */
|
|
9
|
+
actor: {
|
|
10
|
+
profile?: UserProfile;
|
|
11
|
+
user?: AuthUser;
|
|
12
|
+
};
|
|
13
|
+
orgs: IDataProvider['orgs'];
|
|
14
|
+
notifications: INotificationProvider;
|
|
15
|
+
/** Falls back into the org-name slot when the org has no name of its own. */
|
|
16
|
+
fallbackOrgName: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mail an invite's accept link, best-effort.
|
|
20
|
+
*
|
|
21
|
+
* Shared by the mint and resend routes so they cannot disagree about what the
|
|
22
|
+
* invitee receives. Swallows every failure: the invite row is already
|
|
23
|
+
* committed and the caller still holds `acceptUrl` to share by hand, so a dead
|
|
24
|
+
* SMTP host must not report a valid invite as a failure.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deliverInvite(input: DeliverInviteInput): Promise<InviteDelivery>;
|
|
27
|
+
//# sourceMappingURL=deliver-invite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver-invite.d.ts","sourceRoot":"","sources":["../../src/notifications/deliver-invite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,QAAQ,EACR,aAAa,EACb,OAAO,EACP,qBAAqB,EACrB,MAAM,EACN,cAAc,EACd,WAAW,EACX,MAAM,mBAAmB,CAAC;AAG3B,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAElE,MAAM,WAAW,kBAAkB;IAClC,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,WAAW,CAAC;QAAC,IAAI,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;IAClD,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5B,aAAa,EAAE,qBAAqB,CAAC;IACrC,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAuBtF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { renderInviteEmail } from '@selvajs/notifications';
|
|
2
|
+
/**
|
|
3
|
+
* Mail an invite's accept link, best-effort.
|
|
4
|
+
*
|
|
5
|
+
* Shared by the mint and resend routes so they cannot disagree about what the
|
|
6
|
+
* invitee receives. Swallows every failure: the invite row is already
|
|
7
|
+
* committed and the caller still holds `acceptUrl` to share by hand, so a dead
|
|
8
|
+
* SMTP host must not report a valid invite as a failure.
|
|
9
|
+
*/
|
|
10
|
+
export async function deliverInvite(input) {
|
|
11
|
+
const { ctx, log, invite, acceptUrl, actor, orgs, notifications, fallbackOrgName } = input;
|
|
12
|
+
try {
|
|
13
|
+
const org = await orgs.getOrg(ctx, invite.orgId);
|
|
14
|
+
const { status } = await notifications.send(renderInviteEmail({
|
|
15
|
+
to: invite.email,
|
|
16
|
+
acceptUrl,
|
|
17
|
+
orgName: org?.name ?? fallbackOrgName,
|
|
18
|
+
invitedBy: actor.profile?.displayName || actor.user?.email,
|
|
19
|
+
expiresAt: invite.expiresAt
|
|
20
|
+
}), log);
|
|
21
|
+
return status;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
log.warn('Invite mail could not be prepared', {
|
|
25
|
+
component: 'Invites',
|
|
26
|
+
inviteId: invite.id,
|
|
27
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
28
|
+
});
|
|
29
|
+
return 'failed';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=deliver-invite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver-invite.js","sourceRoot":"","sources":["../../src/notifications/deliver-invite.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAiB3D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAyB;IAC5D,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC;IAC3F,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,IAAI,CAC1C,iBAAiB,CAAC;YACjB,EAAE,EAAE,MAAM,CAAC,KAAK;YAChB,SAAS;YACT,OAAO,EAAE,GAAG,EAAE,IAAI,IAAI,eAAe;YACrC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,WAAW,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK;YAC1D,SAAS,EAAE,MAAM,CAAC,SAAS;SAC3B,CAAC,EACF,GAAG,CACH,CAAC;QACF,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,mCAAmC,EAAE;YAC7C,SAAS,EAAE,SAAS;YACpB,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC;IACjB,CAAC;AACF,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { SmtpNotificationProvider } from './smtp-provider.js';
|
|
2
2
|
export { readSmtpConfig, type SmtpConfig, type SmtpEnv } from './smtp-config.js';
|
|
3
|
+
export { deliverInvite, type DeliverInviteInput, type InviteDelivery } from './deliver-invite.js';
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAiC,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAiC,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAgD,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/organizations/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/organizations/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,iCAAiC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ILogger, IOrgStore, OrgMember } from '@selvajs/platform';
|
|
2
|
+
/**
|
|
3
|
+
* Drain every membership page for an org. The admin users pages merge
|
|
4
|
+
* per-user org permissions into these rows; a truncated list would render
|
|
5
|
+
* those users as permissionless, and the permission-toggle UI builds its
|
|
6
|
+
* PATCH payload from the displayed state — so truncation here could silently
|
|
7
|
+
* wipe a user's org permissions on the next edit.
|
|
8
|
+
*/
|
|
9
|
+
export declare function listAllOrgMembers(orgs: IOrgStore, orgId: string, log?: ILogger): Promise<OrgMember[]>;
|
|
10
|
+
//# sourceMappingURL=list-all-members.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-all-members.d.ts","sourceRoot":"","sources":["../../src/organizations/list-all-members.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAQvE;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,GAAG,CAAC,EAAE,OAAO,GACX,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBtB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SYSTEM_CONTEXT } from '@selvajs/platform';
|
|
2
|
+
const PAGE_LIMIT = 200;
|
|
3
|
+
// Runaway guard against an adapter that returns a non-advancing cursor —
|
|
4
|
+
// 100 pages = 20k members, far past any real org on this product.
|
|
5
|
+
const MAX_PAGES = 100;
|
|
6
|
+
/**
|
|
7
|
+
* Drain every membership page for an org. The admin users pages merge
|
|
8
|
+
* per-user org permissions into these rows; a truncated list would render
|
|
9
|
+
* those users as permissionless, and the permission-toggle UI builds its
|
|
10
|
+
* PATCH payload from the displayed state — so truncation here could silently
|
|
11
|
+
* wipe a user's org permissions on the next edit.
|
|
12
|
+
*/
|
|
13
|
+
export async function listAllOrgMembers(orgs, orgId, log) {
|
|
14
|
+
const members = [];
|
|
15
|
+
let cursor;
|
|
16
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
17
|
+
const result = await orgs.listOrgMembers(SYSTEM_CONTEXT, orgId, {
|
|
18
|
+
limit: PAGE_LIMIT,
|
|
19
|
+
cursor
|
|
20
|
+
});
|
|
21
|
+
members.push(...result.items);
|
|
22
|
+
cursor = result.nextCursor;
|
|
23
|
+
if (!cursor)
|
|
24
|
+
return members;
|
|
25
|
+
}
|
|
26
|
+
// Injected rather than resolved: hitting the cap is the one outcome a caller
|
|
27
|
+
// must be able to see, and a package that reached for a host's logger could
|
|
28
|
+
// not be used by a host that has a different one.
|
|
29
|
+
log?.warn('listAllOrgMembers hit the page cap — member list may be incomplete', {
|
|
30
|
+
component: 'admin',
|
|
31
|
+
orgId,
|
|
32
|
+
pages: MAX_PAGES
|
|
33
|
+
});
|
|
34
|
+
return members;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=list-all-members.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-all-members.js","sourceRoot":"","sources":["../../src/organizations/list-all-members.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,yEAAyE;AACzE,kEAAkE;AAClE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,IAAe,EACf,KAAa,EACb,GAAa;IAEb,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,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,cAAc,CAAC,cAAc,EAAE,KAAK,EAAE;YAC/D,KAAK,EAAE,UAAU;YACjB,MAAM;SACN,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC;IAC7B,CAAC;IACD,6EAA6E;IAC7E,4EAA4E;IAC5E,kDAAkD;IAClD,GAAG,EAAE,IAAI,CAAC,oEAAoE,EAAE;QAC/E,SAAS,EAAE,OAAO;QAClB,KAAK;QACL,KAAK,EAAE,SAAS;KAChB,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AAChB,CAAC"}
|