@mj-biz-apps/forms-core-entities-server 0.0.0 → 0.2.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/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/magic-link/FormDistributionEntityServer.d.ts +38 -0
- package/dist/magic-link/FormDistributionEntityServer.d.ts.map +1 -0
- package/dist/magic-link/FormDistributionEntityServer.js +84 -0
- package/dist/magic-link/FormDistributionEntityServer.js.map +1 -0
- package/dist/magic-link/config.d.ts +44 -0
- package/dist/magic-link/config.d.ts.map +1 -0
- package/dist/magic-link/config.js +80 -0
- package/dist/magic-link/config.js.map +1 -0
- package/dist/magic-link/minter.d.ts +93 -0
- package/dist/magic-link/minter.d.ts.map +1 -0
- package/dist/magic-link/minter.js +45 -0
- package/dist/magic-link/minter.js.map +1 -0
- package/dist/magic-link/provision-runner.d.ts +49 -0
- package/dist/magic-link/provision-runner.d.ts.map +1 -0
- package/dist/magic-link/provision-runner.js +66 -0
- package/dist/magic-link/provision-runner.js.map +1 -0
- package/dist/magic-link/provisioning-decision.d.ts +44 -0
- package/dist/magic-link/provisioning-decision.d.ts.map +1 -0
- package/dist/magic-link/provisioning-decision.js +43 -0
- package/dist/magic-link/provisioning-decision.js.map +1 -0
- package/package.json +34 -5
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mj-biz-apps/forms-core-entities-server
|
|
3
|
+
*
|
|
4
|
+
* Server-side entity subclasses for MJ Forms entities. These classes override
|
|
5
|
+
* Save()/Delete()/ValidateAsync() to add lifecycle hooks (anonymous-submission
|
|
6
|
+
* hardening, cross-record invariants, version snapshotting) that must run only
|
|
7
|
+
* on the server.
|
|
8
|
+
*
|
|
9
|
+
* Import this package from the server bootstrap so its @RegisterClass decorators
|
|
10
|
+
* fire at startup.
|
|
11
|
+
*/
|
|
12
|
+
import './magic-link/FormDistributionEntityServer.js';
|
|
13
|
+
export { MagicLinkMinterRegistry, type IAnonymousMagicLinkMinter, type MintAnonymousInviteParams, type MintAnonymousInviteResult, } from './magic-link/minter.js';
|
|
14
|
+
export { getMagicLinkProvisioningConfig, resetMagicLinkProvisioningConfigForTests, type MagicLinkProvisioningConfig, type DistributionChannelType, } from './magic-link/config.js';
|
|
15
|
+
export { decideProvisioning, resolveExpiry, type DistributionProvisioningState, type ProvisioningDecision, } from './magic-link/provisioning-decision.js';
|
|
16
|
+
export { runProvisioning, DISTRIBUTION_ENTITY_NAME, type ProvisionContext, type ProvisionOutcome, } from './magic-link/provision-runner.js';
|
|
17
|
+
export { FormDistributionEntityServer } from './magic-link/FormDistributionEntityServer.js';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,8CAA8C,CAAC;AAKtD,OAAO,EACL,uBAAuB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,GAC/B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,8BAA8B,EAC9B,wCAAwC,EACxC,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,GAC1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,8CAA8C,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mj-biz-apps/forms-core-entities-server
|
|
3
|
+
*
|
|
4
|
+
* Server-side entity subclasses for MJ Forms entities. These classes override
|
|
5
|
+
* Save()/Delete()/ValidateAsync() to add lifecycle hooks (anonymous-submission
|
|
6
|
+
* hardening, cross-record invariants, version snapshotting) that must run only
|
|
7
|
+
* on the server.
|
|
8
|
+
*
|
|
9
|
+
* Import this package from the server bootstrap so its @RegisterClass decorators
|
|
10
|
+
* fire at startup.
|
|
11
|
+
*/
|
|
12
|
+
// FormDistribution lifecycle hook: mints the anonymous magic-link invite when a
|
|
13
|
+
// public/anonymous distribution goes live (FORMS_BUILD_PLAN §4 item 4). The import
|
|
14
|
+
// triggers its @RegisterClass decorator so the class factory uses this subclass.
|
|
15
|
+
import './magic-link/FormDistributionEntityServer.js';
|
|
16
|
+
// The minting seam (interface + registry) and provisioning config/decisions —
|
|
17
|
+
// re-exported so the host bootstrap (`@mj-biz-apps/forms-server`) can register a
|
|
18
|
+
// concrete minter, and so tests can drive the pure decision logic.
|
|
19
|
+
export { MagicLinkMinterRegistry, } from './magic-link/minter.js';
|
|
20
|
+
export { getMagicLinkProvisioningConfig, resetMagicLinkProvisioningConfigForTests, } from './magic-link/config.js';
|
|
21
|
+
export { decideProvisioning, resolveExpiry, } from './magic-link/provisioning-decision.js';
|
|
22
|
+
export { runProvisioning, DISTRIBUTION_ENTITY_NAME, } from './magic-link/provision-runner.js';
|
|
23
|
+
export { FormDistributionEntityServer } from './magic-link/FormDistributionEntityServer.js';
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gFAAgF;AAChF,mFAAmF;AACnF,iFAAiF;AACjF,OAAO,8CAA8C,CAAC;AAEtD,8EAA8E;AAC9E,iFAAiF;AACjF,mEAAmE;AACnE,OAAO,EACL,uBAAuB,GAIxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,8BAA8B,EAC9B,wCAAwC,GAGzC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,kBAAkB,EAClB,aAAa,GAGd,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,eAAe,EACf,wBAAwB,GAGzB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,8CAA8C,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side lifecycle hook for `MJ_BizApps_Forms: Form Distributions`.
|
|
3
|
+
*
|
|
4
|
+
* Mints an anonymous, multi-use, resource-scoped magic-link invite the first
|
|
5
|
+
* time a distribution becomes an active public/anonymous channel, and stores the
|
|
6
|
+
* resulting invite ID on `MagicLinkInviteID`. Fires from `Save()`, so it runs
|
|
7
|
+
* however the distribution is created/activated — builder, AI, import, or API.
|
|
8
|
+
*
|
|
9
|
+
* Registered via `@RegisterClass(BaseEntity, 'MJ_BizApps_Forms: Form Distributions')`
|
|
10
|
+
* so MJ's class factory instantiates THIS subclass server-side. The minting is
|
|
11
|
+
* delegated through the `MagicLinkMinterRegistry` seam (see minter.ts); when no
|
|
12
|
+
* host minter is registered (host has not enabled core `magicLink`), the hook
|
|
13
|
+
* logs a clear warning and leaves `MagicLinkInviteID` null WITHOUT failing the
|
|
14
|
+
* save. The orchestration lives in `runProvisioning` (provision-runner.ts) so it
|
|
15
|
+
* is unit-testable without a DB; this class is the thin BaseEntity adapter.
|
|
16
|
+
*
|
|
17
|
+
* See FORMS_BUILD_PLAN §4 item 4.
|
|
18
|
+
*/
|
|
19
|
+
import { type EntitySaveOptions } from '@memberjunction/core';
|
|
20
|
+
import { mjBizAppsFormsFormDistributionEntity } from '@mj-biz-apps/forms-entities';
|
|
21
|
+
export declare class FormDistributionEntityServer extends mjBizAppsFormsFormDistributionEntity {
|
|
22
|
+
/**
|
|
23
|
+
* Persists the distribution, then (post-save) provisions its anonymous
|
|
24
|
+
* magic-link invite when warranted. Provisioning never blocks the save:
|
|
25
|
+
* a missing minter or a mint failure is logged, and the distribution stands.
|
|
26
|
+
*/
|
|
27
|
+
Save(options?: EntitySaveOptions): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Writes the freshly minted invite ID and raw public-link token back onto this
|
|
30
|
+
* record with a single targeted second save. The re-entry is idempotent: the
|
|
31
|
+
* now-set `MagicLinkInviteID` makes the provisioning decision short-circuit on
|
|
32
|
+
* the recursive `Save()`, so no re-mint occurs. The raw token is written only
|
|
33
|
+
* when the column is still empty, so a repeat call never clobbers it (a public
|
|
34
|
+
* link's redeem URL must stay stable).
|
|
35
|
+
*/
|
|
36
|
+
private persistMintResult;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=FormDistributionEntityServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormDistributionEntityServer.d.ts","sourceRoot":"","sources":["../../src/magic-link/FormDistributionEntityServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEpF,OAAO,EAAE,oCAAoC,EAAE,MAAM,6BAA6B,CAAC;AAKnF,qBACa,4BAA6B,SAAQ,oCAAoC;IACpF;;;;OAIG;IACmB,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkCzE;;;;;;;OAOG;YACW,iBAAiB;CAchC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Server-side lifecycle hook for `MJ_BizApps_Forms: Form Distributions`.
|
|
9
|
+
*
|
|
10
|
+
* Mints an anonymous, multi-use, resource-scoped magic-link invite the first
|
|
11
|
+
* time a distribution becomes an active public/anonymous channel, and stores the
|
|
12
|
+
* resulting invite ID on `MagicLinkInviteID`. Fires from `Save()`, so it runs
|
|
13
|
+
* however the distribution is created/activated — builder, AI, import, or API.
|
|
14
|
+
*
|
|
15
|
+
* Registered via `@RegisterClass(BaseEntity, 'MJ_BizApps_Forms: Form Distributions')`
|
|
16
|
+
* so MJ's class factory instantiates THIS subclass server-side. The minting is
|
|
17
|
+
* delegated through the `MagicLinkMinterRegistry` seam (see minter.ts); when no
|
|
18
|
+
* host minter is registered (host has not enabled core `magicLink`), the hook
|
|
19
|
+
* logs a clear warning and leaves `MagicLinkInviteID` null WITHOUT failing the
|
|
20
|
+
* save. The orchestration lives in `runProvisioning` (provision-runner.ts) so it
|
|
21
|
+
* is unit-testable without a DB; this class is the thin BaseEntity adapter.
|
|
22
|
+
*
|
|
23
|
+
* See FORMS_BUILD_PLAN §4 item 4.
|
|
24
|
+
*/
|
|
25
|
+
import { BaseEntity, LogError } from '@memberjunction/core';
|
|
26
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
27
|
+
import { mjBizAppsFormsFormDistributionEntity } from '@mj-biz-apps/forms-entities';
|
|
28
|
+
import { getMagicLinkProvisioningConfig } from './config.js';
|
|
29
|
+
import { MagicLinkMinterRegistry } from './minter.js';
|
|
30
|
+
import { runProvisioning, DISTRIBUTION_ENTITY_NAME } from './provision-runner.js';
|
|
31
|
+
let FormDistributionEntityServer = class FormDistributionEntityServer extends mjBizAppsFormsFormDistributionEntity {
|
|
32
|
+
/**
|
|
33
|
+
* Persists the distribution, then (post-save) provisions its anonymous
|
|
34
|
+
* magic-link invite when warranted. Provisioning never blocks the save:
|
|
35
|
+
* a missing minter or a mint failure is logged, and the distribution stands.
|
|
36
|
+
*/
|
|
37
|
+
async Save(options) {
|
|
38
|
+
const saved = await super.Save(options);
|
|
39
|
+
if (!saved) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await runProvisioning({
|
|
44
|
+
distributionId: this.ID,
|
|
45
|
+
channelType: this.ChannelType,
|
|
46
|
+
status: this.Status,
|
|
47
|
+
isActive: this.IsActive,
|
|
48
|
+
magicLinkInviteId: this.MagicLinkInviteID,
|
|
49
|
+
closeAt: this.CloseAt,
|
|
50
|
+
}, getMagicLinkProvisioningConfig(), MagicLinkMinterRegistry.Instance.Minter, this.ContextCurrentUser, (mint) => this.persistMintResult(mint));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// Defensive: the runner is fail-soft, but a thrown error must never undo a
|
|
54
|
+
// successful distribution save.
|
|
55
|
+
LogError(`[FormDistributionEntityServer] Provisioning threw for distribution ${this.ID}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Writes the freshly minted invite ID and raw public-link token back onto this
|
|
61
|
+
* record with a single targeted second save. The re-entry is idempotent: the
|
|
62
|
+
* now-set `MagicLinkInviteID` makes the provisioning decision short-circuit on
|
|
63
|
+
* the recursive `Save()`, so no re-mint occurs. The raw token is written only
|
|
64
|
+
* when the column is still empty, so a repeat call never clobbers it (a public
|
|
65
|
+
* link's redeem URL must stay stable).
|
|
66
|
+
*/
|
|
67
|
+
async persistMintResult(mint) {
|
|
68
|
+
this.MagicLinkInviteID = mint.inviteId;
|
|
69
|
+
if (mint.rawToken && !this.PublicLinkToken) {
|
|
70
|
+
this.PublicLinkToken = mint.rawToken;
|
|
71
|
+
}
|
|
72
|
+
if (!(await this.Save())) {
|
|
73
|
+
LogError(`[FormDistributionEntityServer] Failed to store invite ${mint.inviteId} on distribution ${this.ID}: ` +
|
|
74
|
+
`${this.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
FormDistributionEntityServer = __decorate([
|
|
81
|
+
RegisterClass(BaseEntity, DISTRIBUTION_ENTITY_NAME)
|
|
82
|
+
], FormDistributionEntityServer);
|
|
83
|
+
export { FormDistributionEntityServer };
|
|
84
|
+
//# sourceMappingURL=FormDistributionEntityServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormDistributionEntityServer.js","sourceRoot":"","sources":["../../src/magic-link/FormDistributionEntityServer.ts"],"names":[],"mappings":";;;;;;AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAA0B,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,oCAAoC,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAmB,MAAM,uBAAuB,CAAC;AAG5F,IAAM,4BAA4B,GAAlC,MAAM,4BAA6B,SAAQ,oCAAoC;IACpF;;;;OAIG;IACa,KAAK,CAAC,IAAI,CAAC,OAA2B;QACpD,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC;YACH,MAAM,eAAe,CACnB;gBACE,cAAc,EAAE,IAAI,CAAC,EAAE;gBACvB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;gBACzC,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB,EACD,8BAA8B,EAAE,EAChC,uBAAuB,CAAC,QAAQ,CAAC,MAAM,EACvC,IAAI,CAAC,kBAAkB,EACvB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CACvC,CAAC;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,2EAA2E;YAC3E,gCAAgC;YAChC,QAAQ,CACN,sEAAsE,IAAI,CAAC,EAAE,KAC3E,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAC3C,EAAE,CACH,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;OAOG;IACK,KAAK,CAAC,iBAAiB,CAAC,IAAgB;QAC9C,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,QAAQ,CAAC;QACvC,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3C,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,QAAQ,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACzB,QAAQ,CACN,yDAAyD,IAAI,CAAC,QAAQ,oBAAoB,IAAI,CAAC,EAAE,IAAI;gBACnG,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,IAAI,eAAe,EAAE,CAC7D,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AA9DY,4BAA4B;IADxC,aAAa,CAAC,UAAU,EAAE,wBAAwB,CAAC;GACvC,4BAA4B,CA8DxC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for distribution magic-link provisioning.
|
|
3
|
+
*
|
|
4
|
+
* Generic + host-configurable. Every knob is read from `process.env` ONCE and
|
|
5
|
+
* frozen (same pattern as the public-submit config in `@mj-biz-apps/forms-server`),
|
|
6
|
+
* so a host tunes behaviour without code changes and the defaults are safe.
|
|
7
|
+
*
|
|
8
|
+
* Env vars:
|
|
9
|
+
* - `FORMS_MAGICLINK_CHANNELS` Comma-separated `ChannelType` values that get an
|
|
10
|
+
* anonymous link minted. Default `PublicLink,Embed,QR`
|
|
11
|
+
* (NOT `Email` — email distributions are individually
|
|
12
|
+
* addressed, not anonymous public links).
|
|
13
|
+
* - `FORMS_MAGICLINK_MAX_USES` Default `maxUses` for the minted invite. Default
|
|
14
|
+
* 1,000,000 — effectively a public URL. A distribution's
|
|
15
|
+
* own `MaxResponses` quota is enforced separately at submit.
|
|
16
|
+
* - `FORMS_MAGICLINK_EXPIRY_HOURS` Fixed expiry (hours) applied to every minted link.
|
|
17
|
+
* Unset (default) = no fixed expiry; expiry is instead
|
|
18
|
+
* driven by the distribution's `CloseAt` when set.
|
|
19
|
+
* - `FORMS_MAGICLINK_APPLICATION` Application name the anonymous session is scoped to.
|
|
20
|
+
* Default `Forms` (the app WP-A seeds).
|
|
21
|
+
* - `FORMS_MAGICLINK_ROLE` Restricted role the invite grants. Default
|
|
22
|
+
* `Form Respondent` (the role WP-A seeds). Must be
|
|
23
|
+
* magic-link grantable on the host.
|
|
24
|
+
*/
|
|
25
|
+
/** A `ChannelType` value on `FormDistribution`. */
|
|
26
|
+
export type DistributionChannelType = 'Email' | 'Embed' | 'PublicLink' | 'QR';
|
|
27
|
+
/** Frozen, validated configuration for distribution magic-link provisioning. */
|
|
28
|
+
export interface MagicLinkProvisioningConfig {
|
|
29
|
+
/** Channel types that receive a minted anonymous link. */
|
|
30
|
+
linkableChannels: ReadonlySet<DistributionChannelType>;
|
|
31
|
+
/** Default `maxUses` for the minted invite. */
|
|
32
|
+
defaultMaxUses: number;
|
|
33
|
+
/** Fixed expiry in hours, or `undefined` to use the distribution `CloseAt` (or none). */
|
|
34
|
+
fixedExpiryHours: number | undefined;
|
|
35
|
+
/** Application name the anonymous session is scoped to. */
|
|
36
|
+
applicationName: string;
|
|
37
|
+
/** Restricted role the invite grants. */
|
|
38
|
+
roleName: string;
|
|
39
|
+
}
|
|
40
|
+
/** Read (and memoize) the magic-link provisioning configuration from the environment. */
|
|
41
|
+
export declare function getMagicLinkProvisioningConfig(): MagicLinkProvisioningConfig;
|
|
42
|
+
/** Test-only: clear the memoized config so env changes take effect. */
|
|
43
|
+
export declare function resetMagicLinkProvisioningConfigForTests(): void;
|
|
44
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/magic-link/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,mDAAmD;AACnD,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,OAAO,GAAG,YAAY,GAAG,IAAI,CAAC;AAE9E,gFAAgF;AAChF,MAAM,WAAW,2BAA2B;IAC1C,0DAA0D;IAC1D,gBAAgB,EAAE,WAAW,CAAC,uBAAuB,CAAC,CAAC;IACvD,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,yFAAyF;IACzF,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;IACxB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;CAClB;AA4CD,yFAAyF;AACzF,wBAAgB,8BAA8B,IAAI,2BAA2B,CAY5E;AAED,uEAAuE;AACvE,wBAAgB,wCAAwC,IAAI,IAAI,CAE/D"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for distribution magic-link provisioning.
|
|
3
|
+
*
|
|
4
|
+
* Generic + host-configurable. Every knob is read from `process.env` ONCE and
|
|
5
|
+
* frozen (same pattern as the public-submit config in `@mj-biz-apps/forms-server`),
|
|
6
|
+
* so a host tunes behaviour without code changes and the defaults are safe.
|
|
7
|
+
*
|
|
8
|
+
* Env vars:
|
|
9
|
+
* - `FORMS_MAGICLINK_CHANNELS` Comma-separated `ChannelType` values that get an
|
|
10
|
+
* anonymous link minted. Default `PublicLink,Embed,QR`
|
|
11
|
+
* (NOT `Email` — email distributions are individually
|
|
12
|
+
* addressed, not anonymous public links).
|
|
13
|
+
* - `FORMS_MAGICLINK_MAX_USES` Default `maxUses` for the minted invite. Default
|
|
14
|
+
* 1,000,000 — effectively a public URL. A distribution's
|
|
15
|
+
* own `MaxResponses` quota is enforced separately at submit.
|
|
16
|
+
* - `FORMS_MAGICLINK_EXPIRY_HOURS` Fixed expiry (hours) applied to every minted link.
|
|
17
|
+
* Unset (default) = no fixed expiry; expiry is instead
|
|
18
|
+
* driven by the distribution's `CloseAt` when set.
|
|
19
|
+
* - `FORMS_MAGICLINK_APPLICATION` Application name the anonymous session is scoped to.
|
|
20
|
+
* Default `Forms` (the app WP-A seeds).
|
|
21
|
+
* - `FORMS_MAGICLINK_ROLE` Restricted role the invite grants. Default
|
|
22
|
+
* `Form Respondent` (the role WP-A seeds). Must be
|
|
23
|
+
* magic-link grantable on the host.
|
|
24
|
+
*/
|
|
25
|
+
const ALL_CHANNELS = ['Email', 'Embed', 'PublicLink', 'QR'];
|
|
26
|
+
const DEFAULT_CHANNELS = ['PublicLink', 'Embed', 'QR'];
|
|
27
|
+
const DEFAULT_MAX_USES = 1_000_000;
|
|
28
|
+
const DEFAULT_APPLICATION = 'Forms';
|
|
29
|
+
const DEFAULT_ROLE = 'Form Respondent';
|
|
30
|
+
/** Numeric env read with a default; non-numeric / non-positive falls back. */
|
|
31
|
+
function positiveNumberFromEnv(key, fallback) {
|
|
32
|
+
const raw = process.env[key];
|
|
33
|
+
if (raw === undefined || raw.trim() === '') {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
const parsed = Number(raw);
|
|
37
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
38
|
+
}
|
|
39
|
+
/** Optional positive-number env read; returns `undefined` when unset/invalid. */
|
|
40
|
+
function optionalPositiveNumberFromEnv(key) {
|
|
41
|
+
const raw = process.env[key];
|
|
42
|
+
if (raw === undefined || raw.trim() === '') {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const parsed = Number(raw);
|
|
46
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Parse the channel allow-list; unknown tokens are ignored; empty falls back to defaults. */
|
|
49
|
+
function channelsFromEnv(key) {
|
|
50
|
+
const raw = process.env[key];
|
|
51
|
+
if (raw === undefined || raw.trim() === '') {
|
|
52
|
+
return new Set(DEFAULT_CHANNELS);
|
|
53
|
+
}
|
|
54
|
+
const valid = new Set(ALL_CHANNELS);
|
|
55
|
+
const parsed = raw
|
|
56
|
+
.split(',')
|
|
57
|
+
.map((s) => s.trim())
|
|
58
|
+
.filter((s) => valid.has(s));
|
|
59
|
+
return parsed.length > 0 ? new Set(parsed) : new Set(DEFAULT_CHANNELS);
|
|
60
|
+
}
|
|
61
|
+
let cached;
|
|
62
|
+
/** Read (and memoize) the magic-link provisioning configuration from the environment. */
|
|
63
|
+
export function getMagicLinkProvisioningConfig() {
|
|
64
|
+
if (cached) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
cached = Object.freeze({
|
|
68
|
+
linkableChannels: channelsFromEnv('FORMS_MAGICLINK_CHANNELS'),
|
|
69
|
+
defaultMaxUses: positiveNumberFromEnv('FORMS_MAGICLINK_MAX_USES', DEFAULT_MAX_USES),
|
|
70
|
+
fixedExpiryHours: optionalPositiveNumberFromEnv('FORMS_MAGICLINK_EXPIRY_HOURS'),
|
|
71
|
+
applicationName: process.env.FORMS_MAGICLINK_APPLICATION?.trim() || DEFAULT_APPLICATION,
|
|
72
|
+
roleName: process.env.FORMS_MAGICLINK_ROLE?.trim() || DEFAULT_ROLE,
|
|
73
|
+
});
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
/** Test-only: clear the memoized config so env changes take effect. */
|
|
77
|
+
export function resetMagicLinkProvisioningConfigForTests() {
|
|
78
|
+
cached = undefined;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/magic-link/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAmBH,MAAM,YAAY,GAAuC,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;AAChG,MAAM,gBAAgB,GAAuC,CAAC,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAC3F,MAAM,gBAAgB,GAAG,SAAS,CAAC;AACnC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AACpC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC,8EAA8E;AAC9E,SAAS,qBAAqB,CAAC,GAAW,EAAE,QAAgB;IAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AACnE,CAAC;AAED,iFAAiF;AACjF,SAAS,6BAA6B,CAAC,GAAW;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED,8FAA8F;AAC9F,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,OAAO,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,GAAG,CAA0B,YAAY,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,GAAG;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAgC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAA4B,CAAC,CAAC,CAAC;IACxF,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;AACzE,CAAC;AAED,IAAI,MAA+C,CAAC;AAEpD,yFAAyF;AACzF,MAAM,UAAU,8BAA8B;IAC5C,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QACrB,gBAAgB,EAAE,eAAe,CAAC,0BAA0B,CAAC;QAC7D,cAAc,EAAE,qBAAqB,CAAC,0BAA0B,EAAE,gBAAgB,CAAC;QACnF,gBAAgB,EAAE,6BAA6B,CAAC,8BAA8B,CAAC;QAC/E,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,IAAI,EAAE,IAAI,mBAAmB;QACvF,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,YAAY;KACnE,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,wCAAwC;IACtD,MAAM,GAAG,SAAS,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-inversion seam for magic-link invite minting.
|
|
3
|
+
*
|
|
4
|
+
* WHY a seam (and not a direct call into MJ core's `MagicLinkService`):
|
|
5
|
+
* - `@memberjunction/server` does NOT re-export `MagicLinkService`, and its
|
|
6
|
+
* package `exports` map only exposes `.`, so the class cannot be imported
|
|
7
|
+
* from this package (deep imports throw `ERR_PACKAGE_PATH_NOT_EXPORTED`).
|
|
8
|
+
* - Even where reachable, `@memberjunction/server` is heavy (Apollo, the whole
|
|
9
|
+
* GraphQL stack, AI/action bundles) and validates DB config at import time —
|
|
10
|
+
* pulling it into this lightweight entity-subclass package would be wrong.
|
|
11
|
+
* - On the pinned MJ 5.43.0, `MagicLinkService.CreateInvite` cannot set the
|
|
12
|
+
* `IdentityMode='anonymous'` / `Kind='resource-share'` / resource-scope fields
|
|
13
|
+
* a Forms distribution link requires anyway.
|
|
14
|
+
*
|
|
15
|
+
* So this package (the entity-lifecycle layer) defines a minimal minting
|
|
16
|
+
* CONTRACT and a registry; `@mj-biz-apps/forms-server` (which already depends on
|
|
17
|
+
* `@memberjunction/server`) registers a concrete minter at bootstrap. The hook
|
|
18
|
+
* calls whatever is registered, and gates gracefully when nothing is — which is
|
|
19
|
+
* exactly the "host has not enabled magicLink" case. Any host could register a
|
|
20
|
+
* different minting backend without touching this package.
|
|
21
|
+
*/
|
|
22
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
23
|
+
import type { UserInfo } from '@memberjunction/core';
|
|
24
|
+
/** What the hook asks the minter to provision. Generic over any resource. */
|
|
25
|
+
export interface MintAnonymousInviteParams {
|
|
26
|
+
/** Application the anonymous session is scoped to (the minter resolves the ID from this name). */
|
|
27
|
+
applicationName: string;
|
|
28
|
+
/** Restricted role the invite grants (e.g. "Form Respondent"). Must be magic-link grantable on the host. */
|
|
29
|
+
roleName: string;
|
|
30
|
+
/** Resource-share scope: the entity name of the scoped resource (e.g. the distribution entity). */
|
|
31
|
+
resourceTypeName: string;
|
|
32
|
+
/** Resource-share scope: the primary key (stringified) of the scoped resource. */
|
|
33
|
+
resourceId: string;
|
|
34
|
+
/** Maximum redemptions. A public URL uses a high value (effectively unlimited). */
|
|
35
|
+
maxUses: number;
|
|
36
|
+
/** Hard expiry; `null`/`undefined` means the minter applies its own default (typically none / very long). */
|
|
37
|
+
expiresAt?: Date | null;
|
|
38
|
+
}
|
|
39
|
+
/** Outcome of a mint attempt. `skipped` is the graceful-gate signal (NOT an error). */
|
|
40
|
+
export interface MintAnonymousInviteResult {
|
|
41
|
+
/** True when an invite row was created. */
|
|
42
|
+
success: boolean;
|
|
43
|
+
/** The created `MJ: Magic Link Invites` row ID — stored on `FormDistribution.MagicLinkInviteID`. */
|
|
44
|
+
inviteId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The RAW redeemable magic-link token (the secret half of the link). The invite
|
|
47
|
+
* row persists only its SHA-256 hash; this raw value travels in the result so the
|
|
48
|
+
* caller can store it on `FormDistribution.PublicLinkToken` and build the shareable
|
|
49
|
+
* public redeem URL. A public form link is low-secrecy by design (the URL is meant
|
|
50
|
+
* to be shared), so persisting the raw token on the distribution is intentional —
|
|
51
|
+
* the raw token is NOT stored on the invite row.
|
|
52
|
+
*/
|
|
53
|
+
rawToken?: string;
|
|
54
|
+
/**
|
|
55
|
+
* True when minting was deliberately skipped because the host has not enabled
|
|
56
|
+
* magic links (or the minter is otherwise unavailable). The caller must NOT
|
|
57
|
+
* treat this as a failure: it leaves `MagicLinkInviteID` null and logs a warning.
|
|
58
|
+
*/
|
|
59
|
+
skipped?: boolean;
|
|
60
|
+
/** Human-readable reason for a skip or failure. */
|
|
61
|
+
message?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The contract a host registers to provision anonymous magic-link invites.
|
|
65
|
+
* Implemented in `@mj-biz-apps/forms-server` over MJ core's magic-link tables.
|
|
66
|
+
*/
|
|
67
|
+
export interface IAnonymousMagicLinkMinter {
|
|
68
|
+
/**
|
|
69
|
+
* Mints an anonymous, resource-scoped, multi-use invite and returns its ID.
|
|
70
|
+
* Implementations MUST return `{ success:false, skipped:true }` (never throw)
|
|
71
|
+
* when the host has not enabled magic links, so a distribution still saves.
|
|
72
|
+
*
|
|
73
|
+
* @param params what to scope/grant the invite to
|
|
74
|
+
* @param creatingUser the internal staff user saving the distribution (becomes the invite's issuer)
|
|
75
|
+
*/
|
|
76
|
+
MintAnonymousInvite(params: MintAnonymousInviteParams, creatingUser: UserInfo): Promise<MintAnonymousInviteResult>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Process-wide registry holding the host's minter, if any. A `BaseSingleton`
|
|
80
|
+
* per CLAUDE.md rule 6. The hook reads `Instance.Minter`; `undefined` means no
|
|
81
|
+
* host registered one → the hook gates gracefully.
|
|
82
|
+
*/
|
|
83
|
+
export declare class MagicLinkMinterRegistry extends BaseSingleton<MagicLinkMinterRegistry> {
|
|
84
|
+
private _minter;
|
|
85
|
+
static get Instance(): MagicLinkMinterRegistry;
|
|
86
|
+
/** The registered minter, or `undefined` when no host has registered one. */
|
|
87
|
+
get Minter(): IAnonymousMagicLinkMinter | undefined;
|
|
88
|
+
/** Registers (or replaces) the host's minter. Called once at server bootstrap. */
|
|
89
|
+
Register(minter: IAnonymousMagicLinkMinter): void;
|
|
90
|
+
/** Test-only: clears the registered minter. */
|
|
91
|
+
ClearForTests(): void;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=minter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minter.d.ts","sourceRoot":"","sources":["../../src/magic-link/minter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,6EAA6E;AAC7E,MAAM,WAAW,yBAAyB;IACxC,kGAAkG;IAClG,eAAe,EAAE,MAAM,CAAC;IACxB,4GAA4G;IAC5G,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,gBAAgB,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,6GAA6G;IAC7G,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED,uFAAuF;AACvF,MAAM,WAAW,yBAAyB;IACxC,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,oGAAoG;IACpG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;;OAOG;IACH,mBAAmB,CACjB,MAAM,EAAE,yBAAyB,EACjC,YAAY,EAAE,QAAQ,GACrB,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACvC;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,aAAa,CAAC,uBAAuB,CAAC;IACjF,OAAO,CAAC,OAAO,CAAwC;IAEvD,WAAkB,QAAQ,IAAI,uBAAuB,CAEpD;IAED,6EAA6E;IAC7E,IAAW,MAAM,IAAI,yBAAyB,GAAG,SAAS,CAEzD;IAED,kFAAkF;IAC3E,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI;IAIxD,+CAA+C;IACxC,aAAa,IAAI,IAAI;CAG7B"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-inversion seam for magic-link invite minting.
|
|
3
|
+
*
|
|
4
|
+
* WHY a seam (and not a direct call into MJ core's `MagicLinkService`):
|
|
5
|
+
* - `@memberjunction/server` does NOT re-export `MagicLinkService`, and its
|
|
6
|
+
* package `exports` map only exposes `.`, so the class cannot be imported
|
|
7
|
+
* from this package (deep imports throw `ERR_PACKAGE_PATH_NOT_EXPORTED`).
|
|
8
|
+
* - Even where reachable, `@memberjunction/server` is heavy (Apollo, the whole
|
|
9
|
+
* GraphQL stack, AI/action bundles) and validates DB config at import time —
|
|
10
|
+
* pulling it into this lightweight entity-subclass package would be wrong.
|
|
11
|
+
* - On the pinned MJ 5.43.0, `MagicLinkService.CreateInvite` cannot set the
|
|
12
|
+
* `IdentityMode='anonymous'` / `Kind='resource-share'` / resource-scope fields
|
|
13
|
+
* a Forms distribution link requires anyway.
|
|
14
|
+
*
|
|
15
|
+
* So this package (the entity-lifecycle layer) defines a minimal minting
|
|
16
|
+
* CONTRACT and a registry; `@mj-biz-apps/forms-server` (which already depends on
|
|
17
|
+
* `@memberjunction/server`) registers a concrete minter at bootstrap. The hook
|
|
18
|
+
* calls whatever is registered, and gates gracefully when nothing is — which is
|
|
19
|
+
* exactly the "host has not enabled magicLink" case. Any host could register a
|
|
20
|
+
* different minting backend without touching this package.
|
|
21
|
+
*/
|
|
22
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
23
|
+
/**
|
|
24
|
+
* Process-wide registry holding the host's minter, if any. A `BaseSingleton`
|
|
25
|
+
* per CLAUDE.md rule 6. The hook reads `Instance.Minter`; `undefined` means no
|
|
26
|
+
* host registered one → the hook gates gracefully.
|
|
27
|
+
*/
|
|
28
|
+
export class MagicLinkMinterRegistry extends BaseSingleton {
|
|
29
|
+
static get Instance() {
|
|
30
|
+
return super.getInstance();
|
|
31
|
+
}
|
|
32
|
+
/** The registered minter, or `undefined` when no host has registered one. */
|
|
33
|
+
get Minter() {
|
|
34
|
+
return this._minter;
|
|
35
|
+
}
|
|
36
|
+
/** Registers (or replaces) the host's minter. Called once at server bootstrap. */
|
|
37
|
+
Register(minter) {
|
|
38
|
+
this._minter = minter;
|
|
39
|
+
}
|
|
40
|
+
/** Test-only: clears the registered minter. */
|
|
41
|
+
ClearForTests() {
|
|
42
|
+
this._minter = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=minter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minter.js","sourceRoot":"","sources":["../../src/magic-link/minter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AA+DvD;;;;GAIG;AACH,MAAM,OAAO,uBAAwB,SAAQ,aAAsC;IAG1E,MAAM,KAAK,QAAQ;QACxB,OAAO,KAAK,CAAC,WAAW,EAA2B,CAAC;IACtD,CAAC;IAED,6EAA6E;IAC7E,IAAW,MAAM;QACf,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,kFAAkF;IAC3E,QAAQ,CAAC,MAAiC;QAC/C,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,+CAA+C;IACxC,aAAa;QAClB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;IAC3B,CAAC;CACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration for distribution magic-link provisioning, decoupled from the
|
|
3
|
+
* BaseEntity so it is unit-testable without a DB or entity metadata. The entity
|
|
4
|
+
* hook (`FormDistributionEntityServer`) is a thin adapter that supplies this
|
|
5
|
+
* runner with the distribution's current state, the context user, and a
|
|
6
|
+
* persist-callback that writes the minted invite id back onto the record.
|
|
7
|
+
*/
|
|
8
|
+
import { type UserInfo } from '@memberjunction/core';
|
|
9
|
+
import type { MagicLinkProvisioningConfig } from './config.js';
|
|
10
|
+
import { type DistributionProvisioningState } from './provisioning-decision.js';
|
|
11
|
+
import type { IAnonymousMagicLinkMinter } from './minter.js';
|
|
12
|
+
/** Entity name of the distribution itself — the resource the invite is scoped to. */
|
|
13
|
+
export declare const DISTRIBUTION_ENTITY_NAME = "MJ_BizApps_Forms: Form Distributions";
|
|
14
|
+
/** Inputs the runner needs from the saved distribution record. */
|
|
15
|
+
export interface ProvisionContext extends DistributionProvisioningState {
|
|
16
|
+
/** The distribution's primary key (the scoped resource id). */
|
|
17
|
+
distributionId: string;
|
|
18
|
+
}
|
|
19
|
+
/** The persistable artifacts of a successful mint, written back onto the distribution. */
|
|
20
|
+
export interface MintedLink {
|
|
21
|
+
/** The `MJ: Magic Link Invites` row ID → `FormDistribution.MagicLinkInviteID`. */
|
|
22
|
+
inviteId: string;
|
|
23
|
+
/**
|
|
24
|
+
* The raw redeemable token → `FormDistribution.PublicLinkToken`. May be absent
|
|
25
|
+
* if a (non-default) minter does not surface it; the invite is still linked.
|
|
26
|
+
*/
|
|
27
|
+
rawToken?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Outcome of a provisioning run (for logging/assertions; never throws). */
|
|
30
|
+
export interface ProvisionOutcome {
|
|
31
|
+
/** What happened: minted+stored, or why it was skipped. */
|
|
32
|
+
result: 'minted' | 'skipped-decision' | 'skipped-no-minter' | 'skipped-no-user' | 'skipped-minter' | 'mint-failed' | 'store-failed';
|
|
33
|
+
/** The minted invite id, when `result==='minted'`. */
|
|
34
|
+
inviteId?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run the provisioning decision and, when warranted, mint + persist the invite.
|
|
38
|
+
* Fail-soft throughout: a missing minter / user, a minter skip, or a mint/store
|
|
39
|
+
* failure is logged and returned — never thrown — so the caller's save stands.
|
|
40
|
+
*
|
|
41
|
+
* @param ctx current distribution state
|
|
42
|
+
* @param config provisioning configuration
|
|
43
|
+
* @param minter the registered minter, or `undefined` when none is registered
|
|
44
|
+
* @param contextUser the internal staff user saving the record (the invite issuer)
|
|
45
|
+
* @param storeMintResult persists the minted invite id + raw public-link token back
|
|
46
|
+
* onto the record in a single write; returns its save success
|
|
47
|
+
*/
|
|
48
|
+
export declare function runProvisioning(ctx: ProvisionContext, config: MagicLinkProvisioningConfig, minter: IAnonymousMagicLinkMinter | undefined, contextUser: UserInfo | undefined, storeMintResult: (mint: MintedLink) => Promise<boolean>): Promise<ProvisionOutcome>;
|
|
49
|
+
//# sourceMappingURL=provision-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision-runner.d.ts","sourceRoot":"","sources":["../../src/magic-link/provision-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAqC,KAAK,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AACnH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D,qFAAqF;AACrF,eAAO,MAAM,wBAAwB,yCAAyC,CAAC;AAE/E,kEAAkE;AAClE,MAAM,WAAW,gBAAiB,SAAQ,6BAA6B;IACrE,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,0FAA0F;AAC1F,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,MAAM,EAAE,QAAQ,GAAG,kBAAkB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,aAAa,GAAG,cAAc,CAAC;IACpI,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,2BAA2B,EACnC,MAAM,EAAE,yBAAyB,GAAG,SAAS,EAC7C,WAAW,EAAE,QAAQ,GAAG,SAAS,EACjC,eAAe,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,GACtD,OAAO,CAAC,gBAAgB,CAAC,CA2D3B"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration for distribution magic-link provisioning, decoupled from the
|
|
3
|
+
* BaseEntity so it is unit-testable without a DB or entity metadata. The entity
|
|
4
|
+
* hook (`FormDistributionEntityServer`) is a thin adapter that supplies this
|
|
5
|
+
* runner with the distribution's current state, the context user, and a
|
|
6
|
+
* persist-callback that writes the minted invite id back onto the record.
|
|
7
|
+
*/
|
|
8
|
+
import { LogError, LogStatus } from '@memberjunction/core';
|
|
9
|
+
import { decideProvisioning, resolveExpiry } from './provisioning-decision.js';
|
|
10
|
+
/** Entity name of the distribution itself — the resource the invite is scoped to. */
|
|
11
|
+
export const DISTRIBUTION_ENTITY_NAME = 'MJ_BizApps_Forms: Form Distributions';
|
|
12
|
+
/**
|
|
13
|
+
* Run the provisioning decision and, when warranted, mint + persist the invite.
|
|
14
|
+
* Fail-soft throughout: a missing minter / user, a minter skip, or a mint/store
|
|
15
|
+
* failure is logged and returned — never thrown — so the caller's save stands.
|
|
16
|
+
*
|
|
17
|
+
* @param ctx current distribution state
|
|
18
|
+
* @param config provisioning configuration
|
|
19
|
+
* @param minter the registered minter, or `undefined` when none is registered
|
|
20
|
+
* @param contextUser the internal staff user saving the record (the invite issuer)
|
|
21
|
+
* @param storeMintResult persists the minted invite id + raw public-link token back
|
|
22
|
+
* onto the record in a single write; returns its save success
|
|
23
|
+
*/
|
|
24
|
+
export async function runProvisioning(ctx, config, minter, contextUser, storeMintResult) {
|
|
25
|
+
const decision = decideProvisioning(ctx, config);
|
|
26
|
+
if (!decision.shouldMint) {
|
|
27
|
+
return { result: 'skipped-decision' };
|
|
28
|
+
}
|
|
29
|
+
if (!minter) {
|
|
30
|
+
LogStatus(`[FormDistribution] Anonymous links unavailable for distribution ${ctx.distributionId}: ` +
|
|
31
|
+
`no magic-link minter registered. Enable core 'magicLink' (with role '${config.roleName}' grantable) ` +
|
|
32
|
+
`to provision public form links. MagicLinkInviteID left null; the distribution was saved.`);
|
|
33
|
+
return { result: 'skipped-no-minter' };
|
|
34
|
+
}
|
|
35
|
+
if (!contextUser) {
|
|
36
|
+
LogError(`[FormDistribution] Cannot mint magic link for distribution ${ctx.distributionId}: no context user.`);
|
|
37
|
+
return { result: 'skipped-no-user' };
|
|
38
|
+
}
|
|
39
|
+
const mint = await minter.MintAnonymousInvite({
|
|
40
|
+
applicationName: config.applicationName,
|
|
41
|
+
roleName: config.roleName,
|
|
42
|
+
resourceTypeName: DISTRIBUTION_ENTITY_NAME,
|
|
43
|
+
resourceId: ctx.distributionId,
|
|
44
|
+
maxUses: config.defaultMaxUses,
|
|
45
|
+
expiresAt: resolveExpiry(ctx.closeAt, config.fixedExpiryHours, new Date()),
|
|
46
|
+
}, contextUser);
|
|
47
|
+
if (mint.skipped) {
|
|
48
|
+
LogStatus(`[FormDistribution] Anonymous link skipped for distribution ${ctx.distributionId}: ` +
|
|
49
|
+
`${mint.message ?? 'magic-link not enabled on this host'}. MagicLinkInviteID left null; the distribution was saved.`);
|
|
50
|
+
return { result: 'skipped-minter' };
|
|
51
|
+
}
|
|
52
|
+
if (!mint.success || !mint.inviteId) {
|
|
53
|
+
LogError(`[FormDistribution] Magic-link mint failed for distribution ${ctx.distributionId}: ` +
|
|
54
|
+
`${mint.message ?? 'unknown error'}. MagicLinkInviteID left null.`);
|
|
55
|
+
return { result: 'mint-failed' };
|
|
56
|
+
}
|
|
57
|
+
const stored = await storeMintResult({ inviteId: mint.inviteId, rawToken: mint.rawToken });
|
|
58
|
+
if (!stored) {
|
|
59
|
+
LogError(`[FormDistribution] Minted invite ${mint.inviteId} but failed to store it on distribution ${ctx.distributionId}. ` +
|
|
60
|
+
`The invite exists but is not linked.`);
|
|
61
|
+
return { result: 'store-failed', inviteId: mint.inviteId };
|
|
62
|
+
}
|
|
63
|
+
LogStatus(`[FormDistribution] Provisioned anonymous magic-link invite ${mint.inviteId} for distribution ${ctx.distributionId}.`);
|
|
64
|
+
return { result: 'minted', inviteId: mint.inviteId };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=provision-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision-runner.js","sourceRoot":"","sources":["../../src/magic-link/provision-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAiB,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAsC,MAAM,4BAA4B,CAAC;AAGnH,qFAAqF;AACrF,MAAM,CAAC,MAAM,wBAAwB,GAAG,sCAAsC,CAAC;AA2B/E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAqB,EACrB,MAAmC,EACnC,MAA6C,EAC7C,WAAiC,EACjC,eAAuD;IAEvD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IACxC,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,SAAS,CACP,mEAAmE,GAAG,CAAC,cAAc,IAAI;YACvF,wEAAwE,MAAM,CAAC,QAAQ,eAAe;YACtG,0FAA0F,CAC7F,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACzC,CAAC;IAED,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,QAAQ,CAAC,8DAA8D,GAAG,CAAC,cAAc,oBAAoB,CAAC,CAAC;QAC/G,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACvC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAC3C;QACE,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,gBAAgB,EAAE,wBAAwB;QAC1C,UAAU,EAAE,GAAG,CAAC,cAAc;QAC9B,OAAO,EAAE,MAAM,CAAC,cAAc;QAC9B,SAAS,EAAE,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,gBAAgB,EAAE,IAAI,IAAI,EAAE,CAAC;KAC3E,EACD,WAAW,CACZ,CAAC;IAEF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,SAAS,CACP,8DAA8D,GAAG,CAAC,cAAc,IAAI;YAClF,GAAG,IAAI,CAAC,OAAO,IAAI,qCAAqC,4DAA4D,CACvH,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACtC,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,QAAQ,CACN,8DAA8D,GAAG,CAAC,cAAc,IAAI;YAClF,GAAG,IAAI,CAAC,OAAO,IAAI,eAAe,gCAAgC,CACrE,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IACnC,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3F,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,QAAQ,CACN,oCAAoC,IAAI,CAAC,QAAQ,2CAA2C,GAAG,CAAC,cAAc,IAAI;YAChH,sCAAsC,CACzC,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC7D,CAAC;IAED,SAAS,CAAC,8DAA8D,IAAI,CAAC,QAAQ,qBAAqB,GAAG,CAAC,cAAc,GAAG,CAAC,CAAC;IACjI,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for distribution magic-link provisioning — no DB, no MJ
|
|
3
|
+
* runtime, deterministic given its inputs, unit-testable with plain assertions.
|
|
4
|
+
* The entity hook is the imperative shell that wires these decisions to the
|
|
5
|
+
* minter and the entity record.
|
|
6
|
+
*/
|
|
7
|
+
import type { MagicLinkProvisioningConfig, DistributionChannelType } from './config.js';
|
|
8
|
+
/** The subset of `FormDistribution` state the provisioning decision needs. */
|
|
9
|
+
export interface DistributionProvisioningState {
|
|
10
|
+
channelType: DistributionChannelType;
|
|
11
|
+
status: 'Active' | 'Closed' | 'Draft';
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
/** Current value of `MagicLinkInviteID` (null/empty when not yet minted). */
|
|
14
|
+
magicLinkInviteId: string | null;
|
|
15
|
+
/** The distribution's `CloseAt`, if set — drives expiry when no fixed expiry is configured. */
|
|
16
|
+
closeAt: Date | null;
|
|
17
|
+
}
|
|
18
|
+
/** Why provisioning is or isn't warranted for the current save. */
|
|
19
|
+
export interface ProvisioningDecision {
|
|
20
|
+
/** True when the hook should attempt to mint and store an invite. */
|
|
21
|
+
shouldMint: boolean;
|
|
22
|
+
/** Diagnostic reason (for the not-minting case), useful in logs/tests. */
|
|
23
|
+
reason: 'mint' | 'already-minted' | 'channel-not-linkable' | 'not-active-public-channel';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decide whether to mint a magic-link invite for this distribution save.
|
|
27
|
+
*
|
|
28
|
+
* Idempotent + scoped to active public/anonymous channels:
|
|
29
|
+
* - Skip when an invite already exists (`MagicLinkInviteID` set) — never re-mint.
|
|
30
|
+
* - Skip when the channel type is not in the configured linkable set (e.g. Email).
|
|
31
|
+
* - Skip unless the distribution is an active public/anonymous channel
|
|
32
|
+
* (`Status='Active'` AND `IsActive=true`) — a Draft/Closed/disabled distribution
|
|
33
|
+
* gets a link only once it actually goes live.
|
|
34
|
+
*/
|
|
35
|
+
export declare function decideProvisioning(state: DistributionProvisioningState, config: MagicLinkProvisioningConfig): ProvisioningDecision;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the invite's hard expiry for this distribution.
|
|
38
|
+
*
|
|
39
|
+
* Policy: a configured fixed expiry (hours-from-now) wins; otherwise the
|
|
40
|
+
* distribution's `CloseAt` (when set) is the expiry; otherwise no expiry
|
|
41
|
+
* (`null`) — an effectively-permanent public link, matching a high `maxUses`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveExpiry(closeAt: Date | null, fixedExpiryHours: number | undefined, now: Date): Date | null;
|
|
44
|
+
//# sourceMappingURL=provisioning-decision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioning-decision.d.ts","sourceRoot":"","sources":["../../src/magic-link/provisioning-decision.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAExF,8EAA8E;AAC9E,MAAM,WAAW,6BAA6B;IAC5C,WAAW,EAAE,uBAAuB,CAAC;IACrC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,+FAA+F;IAC/F,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,UAAU,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,MAAM,EACF,MAAM,GACN,gBAAgB,GAChB,sBAAsB,GACtB,2BAA2B,CAAC;CACjC;AAOD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,6BAA6B,EACpC,MAAM,EAAE,2BAA2B,GAClC,oBAAoB,CAWtB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,IAAI,GAAG,IAAI,EACpB,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,GAAG,EAAE,IAAI,GACR,IAAI,GAAG,IAAI,CAQb"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Non-empty (trimmed) string guard for the existing invite id. */
|
|
2
|
+
function hasInviteId(id) {
|
|
3
|
+
return typeof id === 'string' && id.trim().length > 0;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Decide whether to mint a magic-link invite for this distribution save.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent + scoped to active public/anonymous channels:
|
|
9
|
+
* - Skip when an invite already exists (`MagicLinkInviteID` set) — never re-mint.
|
|
10
|
+
* - Skip when the channel type is not in the configured linkable set (e.g. Email).
|
|
11
|
+
* - Skip unless the distribution is an active public/anonymous channel
|
|
12
|
+
* (`Status='Active'` AND `IsActive=true`) — a Draft/Closed/disabled distribution
|
|
13
|
+
* gets a link only once it actually goes live.
|
|
14
|
+
*/
|
|
15
|
+
export function decideProvisioning(state, config) {
|
|
16
|
+
if (hasInviteId(state.magicLinkInviteId)) {
|
|
17
|
+
return { shouldMint: false, reason: 'already-minted' };
|
|
18
|
+
}
|
|
19
|
+
if (!config.linkableChannels.has(state.channelType)) {
|
|
20
|
+
return { shouldMint: false, reason: 'channel-not-linkable' };
|
|
21
|
+
}
|
|
22
|
+
if (state.status !== 'Active' || !state.isActive) {
|
|
23
|
+
return { shouldMint: false, reason: 'not-active-public-channel' };
|
|
24
|
+
}
|
|
25
|
+
return { shouldMint: true, reason: 'mint' };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the invite's hard expiry for this distribution.
|
|
29
|
+
*
|
|
30
|
+
* Policy: a configured fixed expiry (hours-from-now) wins; otherwise the
|
|
31
|
+
* distribution's `CloseAt` (when set) is the expiry; otherwise no expiry
|
|
32
|
+
* (`null`) — an effectively-permanent public link, matching a high `maxUses`.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveExpiry(closeAt, fixedExpiryHours, now) {
|
|
35
|
+
if (typeof fixedExpiryHours === 'number' && fixedExpiryHours > 0) {
|
|
36
|
+
return new Date(now.getTime() + fixedExpiryHours * 3600 * 1000);
|
|
37
|
+
}
|
|
38
|
+
if (closeAt instanceof Date && !Number.isNaN(closeAt.getTime())) {
|
|
39
|
+
return closeAt;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=provisioning-decision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioning-decision.js","sourceRoot":"","sources":["../../src/magic-link/provisioning-decision.ts"],"names":[],"mappings":"AA+BA,mEAAmE;AACnE,SAAS,WAAW,CAAC,EAAiB;IACpC,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAoC,EACpC,MAAmC;IAEnC,IAAI,WAAW,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC/D,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;IACpE,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAoB,EACpB,gBAAoC,EACpC,GAAS;IAET,IAAI,OAAO,gBAAgB,KAAK,QAAQ,IAAI,gBAAgB,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,OAAO,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAChE,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mj-biz-apps/forms-core-entities-server",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Server-side entity subclasses for MJ Forms — lifecycle hooks (validation, hardening, cross-record invariants) that run only on the server.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"/dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && tsc-alias -f",
|
|
16
|
+
"test": "vitest run"
|
|
17
|
+
},
|
|
18
|
+
"author": "MemberJunction.com",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"vitest": "^3.2.4"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@mj-biz-apps/forms-entities": "0.2.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@memberjunction/core": "^5.43.0",
|
|
29
|
+
"@memberjunction/core-entities": "^5.43.0",
|
|
30
|
+
"@memberjunction/global": "^5.43.0",
|
|
31
|
+
"@memberjunction/sqlserver-dataprovider": "^5.43.0"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/MemberJunction/bizapps-forms"
|
|
36
|
+
}
|
|
8
37
|
}
|
package/README.md
DELETED