@aglyn/tenant-data-admin 1.0.0-beta.143 → 1.0.0-beta.144
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -4
- package/package.json +6 -6
- package/src/lib/server/collection-preview-token.d.ts +122 -0
- package/src/lib/server/collection-preview-token.js +166 -0
- package/src/lib/server/collection-preview-token.js.map +1 -0
- package/src/lib/server/edit-access-authz.d.ts +25 -0
- package/src/lib/server/edit-access-authz.js +20 -3
- package/src/lib/server/edit-access-authz.js.map +1 -1
- package/src/tenancy.d.ts +1 -0
- package/src/tenancy.js +1 -0
- package/src/tenancy.js.map +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,87 @@
|
|
|
1
|
-
# tenant-data-admin
|
|
1
|
+
# @aglyn/tenant-data-admin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The server data layer of Aglyn's tenant runtime: the Firebase Admin setup,
|
|
4
|
+
typed Firestore converters, and the server-side modules that read and write
|
|
5
|
+
platform data with admin credentials. It is mainly an internal building block
|
|
6
|
+
of `@aglyn/tenant-runtime`, of Aglyn's server apps, and of the server half of
|
|
7
|
+
the `@aglyn/plugins-*` packages. Install it directly only if you are writing
|
|
8
|
+
server code against an Aglyn deployment's Firestore.
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
> Beta. Published from the Aglyn monorepo under the `beta` dist-tag; APIs can change between beta releases.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
npm install @aglyn/tenant-data-admin@beta
|
|
15
|
+
|
|
16
|
+
Peer dependencies:
|
|
17
|
+
|
|
18
|
+
- `firebase-admin`
|
|
19
|
+
- `next` (the render cache uses `next/cache`, and two request handlers use
|
|
20
|
+
Next's API route types)
|
|
21
|
+
- `react` (one module uses React's `cache`)
|
|
22
|
+
|
|
23
|
+
This package is server-only. It uses Node built-ins and admin credentials and
|
|
24
|
+
must not be imported from client code. It depends on `sharp` for image
|
|
25
|
+
variants.
|
|
26
|
+
|
|
27
|
+
## What's in it
|
|
28
|
+
|
|
29
|
+
The root export is one barrel over `src/lib/server`. Its main areas:
|
|
30
|
+
|
|
31
|
+
- Firebase Admin: `firebaseAdmin`, Firestore converters for the core documents
|
|
32
|
+
(`hostConverter`, `screenConverter`, `screenVersionConverter`,
|
|
33
|
+
`layoutConverter`, `layoutVersionConverter`), and ID-token checks such as
|
|
34
|
+
`verifyConsoleIdToken` and `isEmailVerified`.
|
|
35
|
+
- Access: API keys (`mintApiKey`, `verifyApiKey`, `listApiKeys`,
|
|
36
|
+
`revokeApiKey`, `API_SCOPES`), host memberships, organizations, edit-access
|
|
37
|
+
tokens, auth handoff, SSO policy and provisioning, token revocation and
|
|
38
|
+
lockdown.
|
|
39
|
+
- Domains: workspace domains behind a `DomainProvider` seam
|
|
40
|
+
(`domainProvider`), console domains, DNS probing, and email sending domains.
|
|
41
|
+
- Email: suppression, unsubscribe links, topic confirmation, metering, the
|
|
42
|
+
send-rate governor, the marketing gate and sender reputation.
|
|
43
|
+
- Contacts and CRM records: contact upsert, merge, email index, company links,
|
|
44
|
+
list members and CRM activity records.
|
|
45
|
+
- Media: storage paths, signing, download tokens, variants, quarantine,
|
|
46
|
+
tombstones, delivery and the CDN request handler.
|
|
47
|
+
- Privacy: erasure (`erase`, `erase-person`) and personal data export.
|
|
48
|
+
- Abuse limits: rate-limit store, visitor write and console API rate limits,
|
|
49
|
+
password-reset, verify-email and membership-recovery throttles.
|
|
50
|
+
- Organization billing documents (`org-billing`), release flags, notifications, and log-drain
|
|
51
|
+
signature checks.
|
|
52
|
+
|
|
53
|
+
Importing the root has side effects, on purpose: evaluating
|
|
54
|
+
`email-send-rate` and `email-marketing-gate` installs the platform send-rate
|
|
55
|
+
governor and the marketing gate on the shared email sender, so every server
|
|
56
|
+
surface that imports this package gets them without a call.
|
|
57
|
+
|
|
58
|
+
By subpath:
|
|
59
|
+
|
|
60
|
+
- `@aglyn/tenant-data-admin/render-cache` has `withRenderCache`, the cache
|
|
61
|
+
tags (`tenantDataTag`, `tenantHostAliasTag`) and TTL constants used for
|
|
62
|
+
published site data.
|
|
63
|
+
- Any file under `src/lib`, for example
|
|
64
|
+
`@aglyn/tenant-data-admin/server/api-keys`, to import one module without the
|
|
65
|
+
barrel and its side effects.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { verifyApiKey } from '@aglyn/tenant-data-admin/server/api-keys'
|
|
71
|
+
import { withRenderCache } from '@aglyn/tenant-data-admin/render-cache'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { firebaseAdmin, hostConverter } from '@aglyn/tenant-data-admin'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## How it fits
|
|
79
|
+
|
|
80
|
+
This is the data piece of the `tenant` scope in the package map. It imports
|
|
81
|
+
the core (`@aglyn/aglyn`) and generic `@aglyn/shared-*` utilities, and nothing
|
|
82
|
+
above it. `@aglyn/tenant-runtime` reads through it, and a plugin's server code
|
|
83
|
+
may import it; it never imports a plugin.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
Apache-2.0. Source: https://github.com/aglyn/aglyn/tree/main/libs/tenant/data/admin
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-data-admin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.144",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-fbserver": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.144",
|
|
29
|
+
"@aglyn/shared-util-email": "1.0.0-beta.144",
|
|
30
|
+
"@aglyn/shared-util-fbserver": "1.0.0-beta.144",
|
|
31
|
+
"@aglyn/shared-util-http": "1.0.0-beta.144",
|
|
32
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.144",
|
|
33
33
|
"@msgpack/msgpack": "^3.1.3",
|
|
34
34
|
"@swc/helpers": "0.5.23",
|
|
35
35
|
"sharp": "^0.35.3",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A capability to see ONE not-yet-live collection entry on the LIVE site
|
|
19
|
+
* (AGL-3205).
|
|
20
|
+
*
|
|
21
|
+
* The console is `app.aglyn.com`; a tenant is `aglyn.com`, `{sub}.aglyn.app`
|
|
22
|
+
* and every customer's own domain. A console session does not carry to any of
|
|
23
|
+
* them — different registrable domains, storage-partitioned — so "I am logged
|
|
24
|
+
* in, let me see the draft" cannot be answered by a session at all. What DOES
|
|
25
|
+
* carry is a URL, so the capability travels as a signed query parameter and is
|
|
26
|
+
* verified by the tenant server before it loads anything withheld.
|
|
27
|
+
*
|
|
28
|
+
* ## What the signature is over, and why each part is in it
|
|
29
|
+
*
|
|
30
|
+
* `{hostId}:{collectionSlug}:{entrySlug}:{exp}`, under the
|
|
31
|
+
* `collection-preview:` context prefix.
|
|
32
|
+
*
|
|
33
|
+
* - `hostId` — a token minted for one site must not work on another. The
|
|
34
|
+
* tenant compares it against the host it RESOLVED from the request, never
|
|
35
|
+
* against anything the URL says, so presenting `aglyn.com`'s token to
|
|
36
|
+
* another customer's domain fails on the host it actually reached.
|
|
37
|
+
* - `collectionSlug` + `entrySlug` — the scope is one post, not "previews are
|
|
38
|
+
* on for this site". The verifier is handed the slugs parsed from the route
|
|
39
|
+
* and refuses a mismatch, so a token for `/blog/post-a` cannot be edited
|
|
40
|
+
* into `/blog/post-b` or into `/news/post-a` and reveal either.
|
|
41
|
+
* - `exp` — inside the payload, so nobody without the secret can stretch it.
|
|
42
|
+
*
|
|
43
|
+
* ## The secret
|
|
44
|
+
*
|
|
45
|
+
* `TOKEN_SIGNING_SECRET`, the shared fail-closed secret the media, download,
|
|
46
|
+
* stream, edit-hint and form-binding signatures already use — for the reason
|
|
47
|
+
* `media-signing.ts` gives about not adding a second thing to keep in step
|
|
48
|
+
* across two apps. The `collection-preview:` prefix domain-separates it from
|
|
49
|
+
* every one of those, so none of them can be replayed as a preview grant and
|
|
50
|
+
* a preview grant cannot be replayed as any of them.
|
|
51
|
+
*
|
|
52
|
+
* ## What it is NOT
|
|
53
|
+
*
|
|
54
|
+
* It is not an edit session, it names no user, and it grants nothing but
|
|
55
|
+
* "render this one entry as though it were live". The render it authorizes
|
|
56
|
+
* writes nothing: a preview never publishes a due schedule and never records
|
|
57
|
+
* a refusal, because nothing but a PUBLIC render may move a content entry's
|
|
58
|
+
* state (see `flipDueEntry`).
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Two hours.
|
|
62
|
+
*
|
|
63
|
+
* The request this was built for is "send me their preview links" — the link
|
|
64
|
+
* is meant to leave the console, ride a chat message and be opened on whatever
|
|
65
|
+
* device is to hand, so a window measured in minutes fails at the only job it
|
|
66
|
+
* has. Two hours covers a sitting: read three posts, come back after a call.
|
|
67
|
+
*
|
|
68
|
+
* It stays hours and not days because a URL is a bearer capability with no
|
|
69
|
+
* revocation, and because URLs are LOGGED — by the platform, by proxies, by
|
|
70
|
+
* whatever chat client carried it. The same property keeps the media
|
|
71
|
+
* signature at fifteen minutes, the gated-video session at four hours and the
|
|
72
|
+
* edit-hint bounce at sixty seconds. Past one sitting the right answer is to
|
|
73
|
+
* ask the console for another link, which re-checks that the person may still
|
|
74
|
+
* edit that site before it signs anything.
|
|
75
|
+
*/
|
|
76
|
+
export declare const COLLECTION_PREVIEW_TTL_MS: number;
|
|
77
|
+
/**
|
|
78
|
+
* The longest lifetime any preview token may claim, enforced by the VERIFIER
|
|
79
|
+
* as well as the minter — the guard `MEDIA_SIGNATURE_MAX_TTL_MS` describes.
|
|
80
|
+
* `exp` is signed, so this bounds OUR mistakes rather than an attacker's: a
|
|
81
|
+
* minter that one day passes a day where it meant two hours hands out links
|
|
82
|
+
* nothing downstream would otherwise question.
|
|
83
|
+
*/
|
|
84
|
+
export declare const COLLECTION_PREVIEW_MAX_TTL_MS: number;
|
|
85
|
+
export interface CollectionPreviewScope {
|
|
86
|
+
/** The host the entry belongs to — the resolved one, never a claimed one. */
|
|
87
|
+
hostId: string;
|
|
88
|
+
/** The collection's public slug, as the route spells it. */
|
|
89
|
+
collectionSlug: string;
|
|
90
|
+
/** The entry's public slug, as the route spells it. */
|
|
91
|
+
entrySlug: string;
|
|
92
|
+
}
|
|
93
|
+
export interface CollectionPreviewClaims extends CollectionPreviewScope {
|
|
94
|
+
/** Expiry, epoch ms. */
|
|
95
|
+
exp: number;
|
|
96
|
+
}
|
|
97
|
+
export interface MintedCollectionPreviewToken {
|
|
98
|
+
token: string;
|
|
99
|
+
expiresAtMs: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Mints a preview grant for a scope the caller has ALREADY authorized. The
|
|
103
|
+
* mint itself checks nothing but its own arguments — the console route around
|
|
104
|
+
* it proves the caller may edit this site's content before it calls.
|
|
105
|
+
*/
|
|
106
|
+
export declare function mintCollectionPreviewToken(scope: CollectionPreviewScope, nowMs?: number): MintedCollectionPreviewToken;
|
|
107
|
+
/**
|
|
108
|
+
* Whether this presented token authorizes THIS scope right now.
|
|
109
|
+
*
|
|
110
|
+
* Answers a boolean rather than the claims, deliberately: the only thing a
|
|
111
|
+
* caller may do with a verified preview token is reveal the entry the ROUTE
|
|
112
|
+
* already named, so handing back a decoded `entrySlug` would invite a caller
|
|
113
|
+
* to trust the token's spelling over the route's and re-open the substitution
|
|
114
|
+
* this scoping exists to close.
|
|
115
|
+
*
|
|
116
|
+
* Refuses — with no distinction the caller can act on — a token that is
|
|
117
|
+
* malformed, the wrong version, tampered with, expired, claiming a lifetime
|
|
118
|
+
* past {@link COLLECTION_PREVIEW_MAX_TTL_MS}, minted for another host, or
|
|
119
|
+
* minted for another collection or entry. A deploy with no signing secret
|
|
120
|
+
* refuses too, like every signature in this family.
|
|
121
|
+
*/
|
|
122
|
+
export declare function verifyCollectionPreviewToken(token: unknown, scope: CollectionPreviewScope, nowMs?: number): boolean;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { createHmac } from "crypto";
|
|
17
|
+
import { tokenSigningSecret } from "./media-signing.js";
|
|
18
|
+
import { safeEqual } from "./safe-equal.js";
|
|
19
|
+
/**
|
|
20
|
+
* A capability to see ONE not-yet-live collection entry on the LIVE site
|
|
21
|
+
* (AGL-3205).
|
|
22
|
+
*
|
|
23
|
+
* The console is `app.aglyn.com`; a tenant is `aglyn.com`, `{sub}.aglyn.app`
|
|
24
|
+
* and every customer's own domain. A console session does not carry to any of
|
|
25
|
+
* them — different registrable domains, storage-partitioned — so "I am logged
|
|
26
|
+
* in, let me see the draft" cannot be answered by a session at all. What DOES
|
|
27
|
+
* carry is a URL, so the capability travels as a signed query parameter and is
|
|
28
|
+
* verified by the tenant server before it loads anything withheld.
|
|
29
|
+
*
|
|
30
|
+
* ## What the signature is over, and why each part is in it
|
|
31
|
+
*
|
|
32
|
+
* `{hostId}:{collectionSlug}:{entrySlug}:{exp}`, under the
|
|
33
|
+
* `collection-preview:` context prefix.
|
|
34
|
+
*
|
|
35
|
+
* - `hostId` — a token minted for one site must not work on another. The
|
|
36
|
+
* tenant compares it against the host it RESOLVED from the request, never
|
|
37
|
+
* against anything the URL says, so presenting `aglyn.com`'s token to
|
|
38
|
+
* another customer's domain fails on the host it actually reached.
|
|
39
|
+
* - `collectionSlug` + `entrySlug` — the scope is one post, not "previews are
|
|
40
|
+
* on for this site". The verifier is handed the slugs parsed from the route
|
|
41
|
+
* and refuses a mismatch, so a token for `/blog/post-a` cannot be edited
|
|
42
|
+
* into `/blog/post-b` or into `/news/post-a` and reveal either.
|
|
43
|
+
* - `exp` — inside the payload, so nobody without the secret can stretch it.
|
|
44
|
+
*
|
|
45
|
+
* ## The secret
|
|
46
|
+
*
|
|
47
|
+
* `TOKEN_SIGNING_SECRET`, the shared fail-closed secret the media, download,
|
|
48
|
+
* stream, edit-hint and form-binding signatures already use — for the reason
|
|
49
|
+
* `media-signing.ts` gives about not adding a second thing to keep in step
|
|
50
|
+
* across two apps. The `collection-preview:` prefix domain-separates it from
|
|
51
|
+
* every one of those, so none of them can be replayed as a preview grant and
|
|
52
|
+
* a preview grant cannot be replayed as any of them.
|
|
53
|
+
*
|
|
54
|
+
* ## What it is NOT
|
|
55
|
+
*
|
|
56
|
+
* It is not an edit session, it names no user, and it grants nothing but
|
|
57
|
+
* "render this one entry as though it were live". The render it authorizes
|
|
58
|
+
* writes nothing: a preview never publishes a due schedule and never records
|
|
59
|
+
* a refusal, because nothing but a PUBLIC render may move a content entry's
|
|
60
|
+
* state (see `flipDueEntry`).
|
|
61
|
+
*/ /**
|
|
62
|
+
* Two hours.
|
|
63
|
+
*
|
|
64
|
+
* The request this was built for is "send me their preview links" — the link
|
|
65
|
+
* is meant to leave the console, ride a chat message and be opened on whatever
|
|
66
|
+
* device is to hand, so a window measured in minutes fails at the only job it
|
|
67
|
+
* has. Two hours covers a sitting: read three posts, come back after a call.
|
|
68
|
+
*
|
|
69
|
+
* It stays hours and not days because a URL is a bearer capability with no
|
|
70
|
+
* revocation, and because URLs are LOGGED — by the platform, by proxies, by
|
|
71
|
+
* whatever chat client carried it. The same property keeps the media
|
|
72
|
+
* signature at fifteen minutes, the gated-video session at four hours and the
|
|
73
|
+
* edit-hint bounce at sixty seconds. Past one sitting the right answer is to
|
|
74
|
+
* ask the console for another link, which re-checks that the person may still
|
|
75
|
+
* edit that site before it signs anything.
|
|
76
|
+
*/ export const COLLECTION_PREVIEW_TTL_MS = 2 * 60 * 60 * 1000;
|
|
77
|
+
/**
|
|
78
|
+
* The longest lifetime any preview token may claim, enforced by the VERIFIER
|
|
79
|
+
* as well as the minter — the guard `MEDIA_SIGNATURE_MAX_TTL_MS` describes.
|
|
80
|
+
* `exp` is signed, so this bounds OUR mistakes rather than an attacker's: a
|
|
81
|
+
* minter that one day passes a day where it meant two hours hands out links
|
|
82
|
+
* nothing downstream would otherwise question.
|
|
83
|
+
*/ export const COLLECTION_PREVIEW_MAX_TTL_MS = COLLECTION_PREVIEW_TTL_MS;
|
|
84
|
+
/**
|
|
85
|
+
* Allowance for the minting and verifying machines disagreeing about the
|
|
86
|
+
* time. The console signs and the tenant verifies, on different instances —
|
|
87
|
+
* the same minute `media-signing.ts` allows itself.
|
|
88
|
+
*/ const CLOCK_SKEW_MS = 60 * 1000;
|
|
89
|
+
/** Version tag, mirroring the edit-hint token's debuggability rationale. */ const TOKEN_PREFIX = 'aglyn-entry-preview-v1';
|
|
90
|
+
/** The longest token this verifier will even look at. */ const TOKEN_MAX_LENGTH = 4096;
|
|
91
|
+
function signPayload(payloadB64) {
|
|
92
|
+
return createHmac('sha256', tokenSigningSecret()).update(`collection-preview:${payloadB64}`).digest('base64url');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Mints a preview grant for a scope the caller has ALREADY authorized. The
|
|
96
|
+
* mint itself checks nothing but its own arguments — the console route around
|
|
97
|
+
* it proves the caller may edit this site's content before it calls.
|
|
98
|
+
*/ export function mintCollectionPreviewToken(scope, nowMs = Date.now()) {
|
|
99
|
+
const { hostId, collectionSlug, entrySlug } = scope;
|
|
100
|
+
if (!hostId || !collectionSlug || !entrySlug) {
|
|
101
|
+
throw new Error('hostId, collectionSlug and entrySlug are required');
|
|
102
|
+
}
|
|
103
|
+
const exp = nowMs + COLLECTION_PREVIEW_TTL_MS;
|
|
104
|
+
const claims = {
|
|
105
|
+
hostId,
|
|
106
|
+
collectionSlug,
|
|
107
|
+
entrySlug,
|
|
108
|
+
exp
|
|
109
|
+
};
|
|
110
|
+
const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url');
|
|
111
|
+
return {
|
|
112
|
+
token: `${TOKEN_PREFIX}.${payload}.${signPayload(payload)}`,
|
|
113
|
+
expiresAtMs: exp
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether this presented token authorizes THIS scope right now.
|
|
118
|
+
*
|
|
119
|
+
* Answers a boolean rather than the claims, deliberately: the only thing a
|
|
120
|
+
* caller may do with a verified preview token is reveal the entry the ROUTE
|
|
121
|
+
* already named, so handing back a decoded `entrySlug` would invite a caller
|
|
122
|
+
* to trust the token's spelling over the route's and re-open the substitution
|
|
123
|
+
* this scoping exists to close.
|
|
124
|
+
*
|
|
125
|
+
* Refuses — with no distinction the caller can act on — a token that is
|
|
126
|
+
* malformed, the wrong version, tampered with, expired, claiming a lifetime
|
|
127
|
+
* past {@link COLLECTION_PREVIEW_MAX_TTL_MS}, minted for another host, or
|
|
128
|
+
* minted for another collection or entry. A deploy with no signing secret
|
|
129
|
+
* refuses too, like every signature in this family.
|
|
130
|
+
*/ export function verifyCollectionPreviewToken(token, scope, nowMs = Date.now()) {
|
|
131
|
+
if (typeof token !== 'string' || token.length === 0 || token.length > TOKEN_MAX_LENGTH) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const parts = token.split('.');
|
|
135
|
+
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX) return false;
|
|
136
|
+
const [, payload, sig] = parts;
|
|
137
|
+
let expected;
|
|
138
|
+
try {
|
|
139
|
+
expected = signPayload(payload);
|
|
140
|
+
} catch (unused) {
|
|
141
|
+
// Secret missing — refuse rather than trusting anything.
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
// `safeEqual`, not a bare `===`: constant-time on equal-length input and
|
|
145
|
+
// length-checked first, because `timingSafeEqual` throws on the length an
|
|
146
|
+
// attacker controls.
|
|
147
|
+
if (!safeEqual(sig, expected)) return false;
|
|
148
|
+
let claims;
|
|
149
|
+
try {
|
|
150
|
+
claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
151
|
+
} catch (unused) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const exp = Number(claims == null ? void 0 : claims.exp);
|
|
155
|
+
if (!Number.isFinite(exp)) return false;
|
|
156
|
+
if (exp <= nowMs) return false;
|
|
157
|
+
// The platform-mistake bound. Skew is allowed on the FLOOR of the window
|
|
158
|
+
// (the two clocks disagreeing) and not added to the ceiling, so a token that
|
|
159
|
+
// claims more than the maximum is refused however the clocks read.
|
|
160
|
+
if (exp - nowMs > COLLECTION_PREVIEW_MAX_TTL_MS + CLOCK_SKEW_MS) return false;
|
|
161
|
+
// Every part of the scope, and all three compared against what the REQUEST
|
|
162
|
+
// resolved rather than what the token says about itself.
|
|
163
|
+
return claims.hostId === scope.hostId && claims.collectionSlug === scope.collectionSlug && claims.entrySlug === scope.entrySlug;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=collection-preview-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/collection-preview-token.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHmac } from 'crypto'\nimport { tokenSigningSecret } from './media-signing'\nimport { safeEqual } from './safe-equal'\n\n/**\n * A capability to see ONE not-yet-live collection entry on the LIVE site\n * (AGL-3205).\n *\n * The console is `app.aglyn.com`; a tenant is `aglyn.com`, `{sub}.aglyn.app`\n * and every customer's own domain. A console session does not carry to any of\n * them — different registrable domains, storage-partitioned — so \"I am logged\n * in, let me see the draft\" cannot be answered by a session at all. What DOES\n * carry is a URL, so the capability travels as a signed query parameter and is\n * verified by the tenant server before it loads anything withheld.\n *\n * ## What the signature is over, and why each part is in it\n *\n * `{hostId}:{collectionSlug}:{entrySlug}:{exp}`, under the\n * `collection-preview:` context prefix.\n *\n * - `hostId` — a token minted for one site must not work on another. The\n * tenant compares it against the host it RESOLVED from the request, never\n * against anything the URL says, so presenting `aglyn.com`'s token to\n * another customer's domain fails on the host it actually reached.\n * - `collectionSlug` + `entrySlug` — the scope is one post, not \"previews are\n * on for this site\". The verifier is handed the slugs parsed from the route\n * and refuses a mismatch, so a token for `/blog/post-a` cannot be edited\n * into `/blog/post-b` or into `/news/post-a` and reveal either.\n * - `exp` — inside the payload, so nobody without the secret can stretch it.\n *\n * ## The secret\n *\n * `TOKEN_SIGNING_SECRET`, the shared fail-closed secret the media, download,\n * stream, edit-hint and form-binding signatures already use — for the reason\n * `media-signing.ts` gives about not adding a second thing to keep in step\n * across two apps. The `collection-preview:` prefix domain-separates it from\n * every one of those, so none of them can be replayed as a preview grant and\n * a preview grant cannot be replayed as any of them.\n *\n * ## What it is NOT\n *\n * It is not an edit session, it names no user, and it grants nothing but\n * \"render this one entry as though it were live\". The render it authorizes\n * writes nothing: a preview never publishes a due schedule and never records\n * a refusal, because nothing but a PUBLIC render may move a content entry's\n * state (see `flipDueEntry`).\n */\n\n/**\n * Two hours.\n *\n * The request this was built for is \"send me their preview links\" — the link\n * is meant to leave the console, ride a chat message and be opened on whatever\n * device is to hand, so a window measured in minutes fails at the only job it\n * has. Two hours covers a sitting: read three posts, come back after a call.\n *\n * It stays hours and not days because a URL is a bearer capability with no\n * revocation, and because URLs are LOGGED — by the platform, by proxies, by\n * whatever chat client carried it. The same property keeps the media\n * signature at fifteen minutes, the gated-video session at four hours and the\n * edit-hint bounce at sixty seconds. Past one sitting the right answer is to\n * ask the console for another link, which re-checks that the person may still\n * edit that site before it signs anything.\n */\nexport const COLLECTION_PREVIEW_TTL_MS = 2 * 60 * 60 * 1000\n\n/**\n * The longest lifetime any preview token may claim, enforced by the VERIFIER\n * as well as the minter — the guard `MEDIA_SIGNATURE_MAX_TTL_MS` describes.\n * `exp` is signed, so this bounds OUR mistakes rather than an attacker's: a\n * minter that one day passes a day where it meant two hours hands out links\n * nothing downstream would otherwise question.\n */\nexport const COLLECTION_PREVIEW_MAX_TTL_MS = COLLECTION_PREVIEW_TTL_MS\n\n/**\n * Allowance for the minting and verifying machines disagreeing about the\n * time. The console signs and the tenant verifies, on different instances —\n * the same minute `media-signing.ts` allows itself.\n */\nconst CLOCK_SKEW_MS = 60 * 1000\n\n/** Version tag, mirroring the edit-hint token's debuggability rationale. */\nconst TOKEN_PREFIX = 'aglyn-entry-preview-v1'\n\n/** The longest token this verifier will even look at. */\nconst TOKEN_MAX_LENGTH = 4096\n\nexport interface CollectionPreviewScope {\n /** The host the entry belongs to — the resolved one, never a claimed one. */\n hostId: string\n /** The collection's public slug, as the route spells it. */\n collectionSlug: string\n /** The entry's public slug, as the route spells it. */\n entrySlug: string\n}\n\nexport interface CollectionPreviewClaims extends CollectionPreviewScope {\n /** Expiry, epoch ms. */\n exp: number\n}\n\nexport interface MintedCollectionPreviewToken {\n token: string\n expiresAtMs: number\n}\n\nfunction signPayload(payloadB64: string): string {\n return createHmac('sha256', tokenSigningSecret())\n .update(`collection-preview:${payloadB64}`)\n .digest('base64url')\n}\n\n/**\n * Mints a preview grant for a scope the caller has ALREADY authorized. The\n * mint itself checks nothing but its own arguments — the console route around\n * it proves the caller may edit this site's content before it calls.\n */\nexport function mintCollectionPreviewToken(\n scope: CollectionPreviewScope,\n nowMs: number = Date.now(),\n): MintedCollectionPreviewToken {\n const { hostId, collectionSlug, entrySlug } = scope\n if (!hostId || !collectionSlug || !entrySlug) {\n throw new Error('hostId, collectionSlug and entrySlug are required')\n }\n const exp = nowMs + COLLECTION_PREVIEW_TTL_MS\n const claims: CollectionPreviewClaims = { hostId, collectionSlug, entrySlug, exp }\n const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString(\n 'base64url',\n )\n return { token: `${TOKEN_PREFIX}.${payload}.${signPayload(payload)}`, expiresAtMs: exp }\n}\n\n/**\n * Whether this presented token authorizes THIS scope right now.\n *\n * Answers a boolean rather than the claims, deliberately: the only thing a\n * caller may do with a verified preview token is reveal the entry the ROUTE\n * already named, so handing back a decoded `entrySlug` would invite a caller\n * to trust the token's spelling over the route's and re-open the substitution\n * this scoping exists to close.\n *\n * Refuses — with no distinction the caller can act on — a token that is\n * malformed, the wrong version, tampered with, expired, claiming a lifetime\n * past {@link COLLECTION_PREVIEW_MAX_TTL_MS}, minted for another host, or\n * minted for another collection or entry. A deploy with no signing secret\n * refuses too, like every signature in this family.\n */\nexport function verifyCollectionPreviewToken(\n token: unknown,\n scope: CollectionPreviewScope,\n nowMs: number = Date.now(),\n): boolean {\n if (\n typeof token !== 'string' ||\n token.length === 0 ||\n token.length > TOKEN_MAX_LENGTH\n ) {\n return false\n }\n const parts = token.split('.')\n if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX) return false\n const [, payload, sig] = parts\n let expected: string\n try {\n expected = signPayload(payload)\n } catch {\n // Secret missing — refuse rather than trusting anything.\n return false\n }\n // `safeEqual`, not a bare `===`: constant-time on equal-length input and\n // length-checked first, because `timingSafeEqual` throws on the length an\n // attacker controls.\n if (!safeEqual(sig, expected)) return false\n\n let claims: CollectionPreviewClaims\n try {\n claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n } catch {\n return false\n }\n const exp = Number(claims?.exp)\n if (!Number.isFinite(exp)) return false\n if (exp <= nowMs) return false\n // The platform-mistake bound. Skew is allowed on the FLOOR of the window\n // (the two clocks disagreeing) and not added to the ceiling, so a token that\n // claims more than the maximum is refused however the clocks read.\n if (exp - nowMs > COLLECTION_PREVIEW_MAX_TTL_MS + CLOCK_SKEW_MS) return false\n\n // Every part of the scope, and all three compared against what the REQUEST\n // resolved rather than what the token says about itself.\n return (\n claims.hostId === scope.hostId &&\n claims.collectionSlug === scope.collectionSlug &&\n claims.entrySlug === scope.entrySlug\n )\n}\n"],"names":["createHmac","tokenSigningSecret","safeEqual","COLLECTION_PREVIEW_TTL_MS","COLLECTION_PREVIEW_MAX_TTL_MS","CLOCK_SKEW_MS","TOKEN_PREFIX","TOKEN_MAX_LENGTH","signPayload","payloadB64","update","digest","mintCollectionPreviewToken","scope","nowMs","Date","now","hostId","collectionSlug","entrySlug","Error","exp","claims","payload","Buffer","from","JSON","stringify","toString","token","expiresAtMs","verifyCollectionPreviewToken","length","parts","split","sig","expected","parse","Number","isFinite"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,kBAAkB,QAAQ,qBAAiB;AACpD,SAASC,SAAS,QAAQ,kBAAc;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,4BAA4B,IAAI,KAAK,KAAK,KAAI;AAE3D;;;;;;CAMC,GACD,OAAO,MAAMC,gCAAgCD,0BAAyB;AAEtE;;;;CAIC,GACD,MAAME,gBAAgB,KAAK;AAE3B,0EAA0E,GAC1E,MAAMC,eAAe;AAErB,uDAAuD,GACvD,MAAMC,mBAAmB;AAqBzB,SAASC,YAAYC,UAAkB;IACrC,OAAOT,WAAW,UAAUC,sBACzBS,MAAM,CAAC,CAAC,mBAAmB,EAAED,YAAY,EACzCE,MAAM,CAAC;AACZ;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdC,KAA6B,EAC7BC,QAAgBC,KAAKC,GAAG,EAAE;IAE1B,MAAM,EAAEC,MAAM,EAAEC,cAAc,EAAEC,SAAS,EAAE,GAAGN;IAC9C,IAAI,CAACI,UAAU,CAACC,kBAAkB,CAACC,WAAW;QAC5C,MAAM,IAAIC,MAAM;IAClB;IACA,MAAMC,MAAMP,QAAQX;IACpB,MAAMmB,SAAkC;QAAEL;QAAQC;QAAgBC;QAAWE;IAAI;IACjF,MAAME,UAAUC,OAAOC,IAAI,CAACC,KAAKC,SAAS,CAACL,SAAS,QAAQM,QAAQ,CAClE;IAEF,OAAO;QAAEC,OAAO,GAAGvB,aAAa,CAAC,EAAEiB,QAAQ,CAAC,EAAEf,YAAYe,UAAU;QAAEO,aAAaT;IAAI;AACzF;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASU,6BACdF,KAAc,EACdhB,KAA6B,EAC7BC,QAAgBC,KAAKC,GAAG,EAAE;IAE1B,IACE,OAAOa,UAAU,YACjBA,MAAMG,MAAM,KAAK,KACjBH,MAAMG,MAAM,GAAGzB,kBACf;QACA,OAAO;IACT;IACA,MAAM0B,QAAQJ,MAAMK,KAAK,CAAC;IAC1B,IAAID,MAAMD,MAAM,KAAK,KAAKC,KAAK,CAAC,EAAE,KAAK3B,cAAc,OAAO;IAC5D,MAAM,GAAGiB,SAASY,IAAI,GAAGF;IACzB,IAAIG;IACJ,IAAI;QACFA,WAAW5B,YAAYe;IACzB,EAAE,eAAM;QACN,yDAAyD;QACzD,OAAO;IACT;IACA,yEAAyE;IACzE,0EAA0E;IAC1E,qBAAqB;IACrB,IAAI,CAACrB,UAAUiC,KAAKC,WAAW,OAAO;IAEtC,IAAId;IACJ,IAAI;QACFA,SAASI,KAAKW,KAAK,CAACb,OAAOC,IAAI,CAACF,SAAS,aAAaK,QAAQ,CAAC;IACjE,EAAE,eAAM;QACN,OAAO;IACT;IACA,MAAMP,MAAMiB,OAAOhB,0BAAAA,OAAQD,GAAG;IAC9B,IAAI,CAACiB,OAAOC,QAAQ,CAAClB,MAAM,OAAO;IAClC,IAAIA,OAAOP,OAAO,OAAO;IACzB,yEAAyE;IACzE,6EAA6E;IAC7E,mEAAmE;IACnE,IAAIO,MAAMP,QAAQV,gCAAgCC,eAAe,OAAO;IAExE,2EAA2E;IAC3E,yDAAyD;IACzD,OACEiB,OAAOL,MAAM,KAAKJ,MAAMI,MAAM,IAC9BK,OAAOJ,cAAc,KAAKL,MAAMK,cAAc,IAC9CI,OAAOH,SAAS,KAAKN,MAAMM,SAAS;AAExC"}
|
|
@@ -54,3 +54,28 @@ export declare function editAccessMintRefusal(options: {
|
|
|
54
54
|
/** From verified ID-token claims; a hint carries none, so it stays false. */
|
|
55
55
|
staff?: boolean;
|
|
56
56
|
}): Promise<Response | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Steps 2–4 of the gate above, WITHOUT the edit-bar release flag (AGL-3205).
|
|
59
|
+
*
|
|
60
|
+
* "May this uid edit this host's content?" is a question more than one feature
|
|
61
|
+
* asks. The live-site preview of an unpublished entry asks it — the link it
|
|
62
|
+
* mints reveals a post the public cannot see, so the person asking for one has
|
|
63
|
+
* to be someone who could have published it — and it is emphatically NOT the
|
|
64
|
+
* edit bar: `release_edit_bar` is that surface's kill switch, and a preview
|
|
65
|
+
* link has no business disappearing when somebody turns the bar off.
|
|
66
|
+
*
|
|
67
|
+
* Split rather than copied, for the reason this module exists at all: the
|
|
68
|
+
* whole note above is about not growing a second authorization path that
|
|
69
|
+
* drifts from the first. Callers that ARE the edit bar keep calling
|
|
70
|
+
* {@link editAccessMintRefusal} and keep its flag.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hostContentEditRefusal(options: {
|
|
73
|
+
request: Request;
|
|
74
|
+
firestore: Firestore;
|
|
75
|
+
/** The `hosts/{hostId}` snapshot, already confirmed to exist. */
|
|
76
|
+
host: DocumentSnapshot;
|
|
77
|
+
orgId: string;
|
|
78
|
+
uid: string;
|
|
79
|
+
/** From verified ID-token claims; a hint carries none, so it stays false. */
|
|
80
|
+
staff?: boolean;
|
|
81
|
+
}): Promise<Response | null>;
|
|
@@ -46,15 +46,32 @@ import { isServerReleaseFlagOnForOrg } from "./release-flags.js";
|
|
|
46
46
|
* console, a signed hint at the tenant exchange) — identity is the caller's
|
|
47
47
|
* business; permission is this function's.
|
|
48
48
|
*/ export async function editAccessMintRefusal(options) {
|
|
49
|
-
|
|
50
|
-
const { request, firestore, host, orgId, uid, staff = false } = options;
|
|
51
|
-
if (!await isServerReleaseFlagOnForOrg('release_edit_bar', orgId)) {
|
|
49
|
+
if (!await isServerReleaseFlagOnForOrg('release_edit_bar', options.orgId)) {
|
|
52
50
|
return Response.json({
|
|
53
51
|
error: 'Not available'
|
|
54
52
|
}, {
|
|
55
53
|
status: 404
|
|
56
54
|
});
|
|
57
55
|
}
|
|
56
|
+
return hostContentEditRefusal(options);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Steps 2–4 of the gate above, WITHOUT the edit-bar release flag (AGL-3205).
|
|
60
|
+
*
|
|
61
|
+
* "May this uid edit this host's content?" is a question more than one feature
|
|
62
|
+
* asks. The live-site preview of an unpublished entry asks it — the link it
|
|
63
|
+
* mints reveals a post the public cannot see, so the person asking for one has
|
|
64
|
+
* to be someone who could have published it — and it is emphatically NOT the
|
|
65
|
+
* edit bar: `release_edit_bar` is that surface's kill switch, and a preview
|
|
66
|
+
* link has no business disappearing when somebody turns the bar off.
|
|
67
|
+
*
|
|
68
|
+
* Split rather than copied, for the reason this module exists at all: the
|
|
69
|
+
* whole note above is about not growing a second authorization path that
|
|
70
|
+
* drifts from the first. Callers that ARE the edit bar keep calling
|
|
71
|
+
* {@link editAccessMintRefusal} and keep its flag.
|
|
72
|
+
*/ export async function hostContentEditRefusal(options) {
|
|
73
|
+
var _membership_data, _host_get, _ref;
|
|
74
|
+
const { request, firestore, host, orgId, uid, staff = false } = options;
|
|
58
75
|
const membership = await firestore.collection('orgs').doc(orgId).collection('members').doc(uid).get();
|
|
59
76
|
if (!membership.exists) {
|
|
60
77
|
return Response.json({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/edit-access-authz.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AglynOrgMember,\n hostRoleCanWrite,\n hostRoleFor,\n isOrgWideMember,\n} from '@aglyn/aglyn/server'\nimport type { DocumentSnapshot, Firestore } from 'firebase-admin/firestore'\nimport { lockdownRefusal } from './lockdown'\nimport { getOrgDoc } from './organizations'\nimport { isServerReleaseFlagOnForOrg } from './release-flags'\n\n/**\n * THE authorization gate for minting an edit-access token — extracted\n * verbatim from the console's `/api/edit-access/token` (AGL-1302 follow-on)\n * so the tenant's `/api/edit-access/exchange` (AGL-1842) evaluates the SAME\n * question the same way, rather than growing a second authorization path\n * that would drift from the first.\n *\n * The gate, in order:\n *\n * 1. `release_edit_bar` for the host's org — on for every site since its\n * release (AGL-3041), and the kill switch for the whole surface, for\n * everyone or for one org by staff override;\n * 2. org roster membership — proven against the host the CALLER was already\n * resolved to, never a caller-supplied orgId;\n * 3. the co-editing edit gate the presence broker applies, for THIS host: a\n * host role that may write content (admin, editor or author), from the\n * host's `memberRoles` or the member's own `hostAccess`, or an org role\n * above viewer held org-wide (AGL-3062). A viewer gets a plain 403 rather\n * than a read-only bar;\n * 4. the lockdown verdict (AGL-1506) at `intent: 'write'` — the reasoning\n * for `write` on a stateless mint is recorded at length where this code\n * came from and holds unchanged: the token buys entry to an editor whose\n * saves a read-only lock denies, and it outlives this check by its TTL.\n *\n * Returns the refusal Response to send, or `null` when the uid may edit the\n * host. Callers verify WHO the uid is first (a Firebase ID token at the\n * console, a signed hint at the tenant exchange) — identity is the caller's\n * business; permission is this function's.\n */\nexport async function editAccessMintRefusal(options: {\n request: Request\n firestore: Firestore\n /** The `hosts/{hostId}` snapshot, already confirmed to exist. */\n host: DocumentSnapshot\n orgId: string\n uid: string\n /** From verified ID-token claims; a hint carries none, so it stays false. */\n staff?: boolean\n}): Promise<Response | null> {\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/edit-access-authz.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AglynOrgMember,\n hostRoleCanWrite,\n hostRoleFor,\n isOrgWideMember,\n} from '@aglyn/aglyn/server'\nimport type { DocumentSnapshot, Firestore } from 'firebase-admin/firestore'\nimport { lockdownRefusal } from './lockdown'\nimport { getOrgDoc } from './organizations'\nimport { isServerReleaseFlagOnForOrg } from './release-flags'\n\n/**\n * THE authorization gate for minting an edit-access token — extracted\n * verbatim from the console's `/api/edit-access/token` (AGL-1302 follow-on)\n * so the tenant's `/api/edit-access/exchange` (AGL-1842) evaluates the SAME\n * question the same way, rather than growing a second authorization path\n * that would drift from the first.\n *\n * The gate, in order:\n *\n * 1. `release_edit_bar` for the host's org — on for every site since its\n * release (AGL-3041), and the kill switch for the whole surface, for\n * everyone or for one org by staff override;\n * 2. org roster membership — proven against the host the CALLER was already\n * resolved to, never a caller-supplied orgId;\n * 3. the co-editing edit gate the presence broker applies, for THIS host: a\n * host role that may write content (admin, editor or author), from the\n * host's `memberRoles` or the member's own `hostAccess`, or an org role\n * above viewer held org-wide (AGL-3062). A viewer gets a plain 403 rather\n * than a read-only bar;\n * 4. the lockdown verdict (AGL-1506) at `intent: 'write'` — the reasoning\n * for `write` on a stateless mint is recorded at length where this code\n * came from and holds unchanged: the token buys entry to an editor whose\n * saves a read-only lock denies, and it outlives this check by its TTL.\n *\n * Returns the refusal Response to send, or `null` when the uid may edit the\n * host. Callers verify WHO the uid is first (a Firebase ID token at the\n * console, a signed hint at the tenant exchange) — identity is the caller's\n * business; permission is this function's.\n */\nexport async function editAccessMintRefusal(options: {\n request: Request\n firestore: Firestore\n /** The `hosts/{hostId}` snapshot, already confirmed to exist. */\n host: DocumentSnapshot\n orgId: string\n uid: string\n /** From verified ID-token claims; a hint carries none, so it stays false. */\n staff?: boolean\n}): Promise<Response | null> {\n if (!(await isServerReleaseFlagOnForOrg('release_edit_bar', options.orgId))) {\n return Response.json({ error: 'Not available' }, { status: 404 })\n }\n return hostContentEditRefusal(options)\n}\n\n/**\n * Steps 2–4 of the gate above, WITHOUT the edit-bar release flag (AGL-3205).\n *\n * \"May this uid edit this host's content?\" is a question more than one feature\n * asks. The live-site preview of an unpublished entry asks it — the link it\n * mints reveals a post the public cannot see, so the person asking for one has\n * to be someone who could have published it — and it is emphatically NOT the\n * edit bar: `release_edit_bar` is that surface's kill switch, and a preview\n * link has no business disappearing when somebody turns the bar off.\n *\n * Split rather than copied, for the reason this module exists at all: the\n * whole note above is about not growing a second authorization path that\n * drifts from the first. Callers that ARE the edit bar keep calling\n * {@link editAccessMintRefusal} and keep its flag.\n */\nexport async function hostContentEditRefusal(options: {\n request: Request\n firestore: Firestore\n /** The `hosts/{hostId}` snapshot, already confirmed to exist. */\n host: DocumentSnapshot\n orgId: string\n uid: string\n /** From verified ID-token claims; a hint carries none, so it stays false. */\n staff?: boolean\n}): Promise<Response | null> {\n const { request, firestore, host, orgId, uid, staff = false } = options\n\n const membership = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(uid)\n .get()\n if (!membership.exists) {\n return Response.json({ error: 'Not a member of this site' }, { status: 403 })\n }\n\n // A roster row proves membership of the ORG, not reach to this host. The\n // roster holds site collaborators too, and one can carry the org role\n // `editor` with `allHosts: false` and a `hostAccess` map naming only their\n // own sites — so the org-role arm counts only when the member is org-wide,\n // exactly as the presence broker asks it (AGL-1881). Otherwise that\n // collaborator would be minted a token for every site in the org, and the\n // bar would show them another site's screens and today's traffic\n // (AGL-3062).\n //\n // `hostRoleFor` reads `hostAccess`/`allHosts` for this host, and\n // `isOrgWideMember` sits beside it for the one shape the two read\n // differently: a pre-`allHosts` row with no `hostAccess` is org-wide, and\n // `hostRoleFor` alone would lock that member out of their own sites.\n const member = (membership.data() ?? {}) as Partial<AglynOrgMember>\n const orgRole = member.role\n const hostRole = ((host.get('memberRoles') ?? {}) as Record<string, string>)[\n uid\n ]\n // `author` (AGL-2334) — the edit-bar entry token. Same reasoning as the\n // presence broker: this buys entry to the editor, and an author's whole\n // purpose is to be in it. What they cannot do once inside is enforced by\n // the rules on the publish fields.\n const canEdit =\n hostRoleCanWrite(hostRole) ||\n hostRoleCanWrite(hostRoleFor(member, host.id)) ||\n (isOrgWideMember(member) &&\n (orgRole === 'owner' || orgRole === 'admin' || orgRole === 'editor'))\n if (!canEdit) {\n return Response.json({ error: 'No edit access' }, { status: 403 })\n }\n\n // Host doc already in hand; the org scope rides the member doc's\n // `orgSuspended` projection (also already read) — the org doc is fetched\n // only when the projection trips, so the happy path adds no org read.\n return lockdownRefusal({\n request,\n intent: 'write',\n staff,\n uid,\n org:\n membership.get('orgSuspended') === true\n ? ((await getOrgDoc(orgId)) ?? {})\n : undefined,\n host: host.data(),\n })\n}\n"],"names":["hostRoleCanWrite","hostRoleFor","isOrgWideMember","lockdownRefusal","getOrgDoc","isServerReleaseFlagOnForOrg","editAccessMintRefusal","options","orgId","Response","json","error","status","hostContentEditRefusal","membership","host","request","firestore","uid","staff","collection","doc","get","exists","member","data","orgRole","role","hostRole","canEdit","id","intent","org","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,gBAAgB,EAChBC,WAAW,EACXC,eAAe,QACV,sBAAqB;AAE5B,SAASC,eAAe,QAAQ,gBAAY;AAC5C,SAASC,SAAS,QAAQ,qBAAiB;AAC3C,SAASC,2BAA2B,QAAQ,qBAAiB;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,eAAeC,sBAAsBC,OAS3C;IACC,IAAI,CAAE,MAAMF,4BAA4B,oBAAoBE,QAAQC,KAAK,GAAI;QAC3E,OAAOC,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAgB,GAAG;YAAEC,QAAQ;QAAI;IACjE;IACA,OAAOC,uBAAuBN;AAChC;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeM,uBAAuBN,OAS5C;QA0BiBO,kBAEGC,WA0BT;IArDV,MAAM,EAAEC,OAAO,EAAEC,SAAS,EAAEF,IAAI,EAAEP,KAAK,EAAEU,GAAG,EAAEC,QAAQ,KAAK,EAAE,GAAGZ;IAEhE,MAAMO,aAAa,MAAMG,UACtBG,UAAU,CAAC,QACXC,GAAG,CAACb,OACJY,UAAU,CAAC,WACXC,GAAG,CAACH,KACJI,GAAG;IACN,IAAI,CAACR,WAAWS,MAAM,EAAE;QACtB,OAAOd,SAASC,IAAI,CAAC;YAAEC,OAAO;QAA4B,GAAG;YAAEC,QAAQ;QAAI;IAC7E;IAEA,yEAAyE;IACzE,sEAAsE;IACtE,2EAA2E;IAC3E,2EAA2E;IAC3E,oEAAoE;IACpE,0EAA0E;IAC1E,iEAAiE;IACjE,cAAc;IACd,EAAE;IACF,iEAAiE;IACjE,kEAAkE;IAClE,0EAA0E;IAC1E,qEAAqE;IACrE,MAAMY,UAAUV,mBAAAA,WAAWW,IAAI,cAAfX,mBAAqB,CAAC;IACtC,MAAMY,UAAUF,OAAOG,IAAI;IAC3B,MAAMC,WAAW,EAAEb,YAAAA,KAAKO,GAAG,CAAC,0BAATP,YAA2B,CAAC,EAA6B,CAC1EG,IACD;IACD,wEAAwE;IACxE,wEAAwE;IACxE,yEAAyE;IACzE,mCAAmC;IACnC,MAAMW,UACJ7B,iBAAiB4B,aACjB5B,iBAAiBC,YAAYuB,QAAQT,KAAKe,EAAE,MAC3C5B,gBAAgBsB,WACdE,CAAAA,YAAY,WAAWA,YAAY,WAAWA,YAAY,QAAO;IACtE,IAAI,CAACG,SAAS;QACZ,OAAOpB,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAiB,GAAG;YAAEC,QAAQ;QAAI;IAClE;IAEA,iEAAiE;IACjE,yEAAyE;IACzE,sEAAsE;IACtE,OAAOT,gBAAgB;QACrBa;QACAe,QAAQ;QACRZ;QACAD;QACAc,KACElB,WAAWQ,GAAG,CAAC,oBAAoB,QAC7B,OAAA,MAAMlB,UAAUI,kBAAhB,OAA2B,CAAC,IAC9ByB;QACNlB,MAAMA,KAAKU,IAAI;IACjB;AACF"}
|
package/src/tenancy.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export * from './lib/server/email-flow-gate';
|
|
|
44
44
|
export * from './lib/server/email-marketing-gate';
|
|
45
45
|
export * from './lib/server/sms-keywords';
|
|
46
46
|
export * from './lib/server/document-id';
|
|
47
|
+
export * from './lib/server/collection-preview-token';
|
|
47
48
|
export * from './lib/server/edit-access-authz';
|
|
48
49
|
export * from './lib/server/edit-access-token';
|
|
49
50
|
export * from './lib/server/edit-hint-token';
|
package/src/tenancy.js
CHANGED
|
@@ -53,6 +53,7 @@ export * from "./lib/server/email-flow-gate.js";
|
|
|
53
53
|
export * from "./lib/server/email-marketing-gate.js";
|
|
54
54
|
export * from "./lib/server/sms-keywords.js";
|
|
55
55
|
export * from "./lib/server/document-id.js";
|
|
56
|
+
export * from "./lib/server/collection-preview-token.js";
|
|
56
57
|
export * from "./lib/server/edit-access-authz.js";
|
|
57
58
|
export * from "./lib/server/edit-access-token.js";
|
|
58
59
|
export * from "./lib/server/edit-hint-token.js";
|
package/src/tenancy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/data/admin/src/tenancy.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/server/analytics-retention'\nexport * from './lib/server/api-http'\nexport * from './lib/server/api-keys'\nexport * from './lib/server/account-emails'\nexport * from './lib/server/auth-pools'\nexport * from './lib/server/client-error-report'\nexport * from './lib/server/connect-account-status'\nexport * from './lib/server/connect-payout-failure'\nexport * from './lib/server/stripe-account-mode'\nexport * from './lib/server/auth-handoff'\nexport * from './lib/server/console-domains'\nexport * from './lib/server/dns-probe'\nexport * from './lib/server/sending-domains'\nexport * from './lib/server/host-sending-domain'\nexport * from './lib/server/sending-domain-debt'\nexport * from './lib/server/sending-domain-recheck'\n// The hosting seam itself, so a door can ask whether this deployment is able\n// to register a name at all before it promises the caller one.\n// `workspace-domains` re-exports the pieces its own callers share with it; the\n// provider behind it is not among them.\nexport { domainProvider } from './lib/server/domain-provider'\nexport type {\n DomainProvider,\n DomainProviderId,\n DomainScope,\n} from './lib/server/domain-provider'\nexport * from './lib/server/platform-domain-names'\nexport * from './lib/server/consume-once'\nexport * from './lib/server/csp-aggregate'\nexport * from './lib/server/data-storage-gate'\nexport * from './lib/server/contact-suppression'\nexport * from './lib/server/email-suppression'\n// The double opt-in quarantine, after the module whose keying it shares and\n// whose `filterTopicSendable` is the reader that makes it a quarantine rather\n// than a stored fact.\nexport * from './lib/server/email-topic-confirmation'\nexport * from './lib/server/email-unsubscribe-link'\nexport * from './lib/server/email-flow-gate'\n// Side-effecting on purpose, for the reason `email-send-rate` below is:\n// evaluating this module INSTALLS the marketing gate on `sendEmail`, so the\n// unsubscribe header, the suppression check and the frequency ceiling reach\n// every bulk sender without a call at each server entrypoint.\nexport * from './lib/server/email-marketing-gate'\nexport * from './lib/server/sms-keywords'\nexport * from './lib/server/document-id'\nexport * from './lib/server/edit-access-authz'\nexport * from './lib/server/edit-access-token'\nexport * from './lib/server/edit-hint-token'\nexport * from './lib/server/email-metering'\n// Side-effecting on purpose (AGL-2409): evaluating this module INSTALLS the\n// platform send-rate governor on `sendEmail`. Every server surface already\n// imports this barrel, so nothing has to remember to call an installer.\nexport * from './lib/server/campaign-conversion-attribution'\nexport * from './lib/server/email-send-rate'\nexport * from './lib/server/email-sender-reputation'\nexport * from './lib/server/erase'\nexport * from './lib/server/erase-person'\nexport * from './lib/server/dynamic-list-materialize'\nexport * from './lib/server/firebase-admin'\nexport * from './lib/server/free-workspace-cap'\nexport * from './lib/server/ga4-measurement-protocol'\nexport * from './lib/server/host-memberships'\nexport * from './lib/server/host-visitor-records'\nexport * from './lib/server/list-members'\nexport * from './lib/server/lockdown'\nexport * from './lib/server/token-revocation'\nexport * from './lib/server/tenant-write-lockdown'\nexport * from './lib/server/host-email-tokens'\nexport * from './lib/server/notifications'\nexport * from './lib/server/org-billing'\nexport * from './lib/server/organizations'\nexport * from './lib/server/duplicate-activity'\nexport * from './lib/server/duplicate-resource'\nexport * from './lib/server/workspace-domains'\nexport * from './lib/server/membership-recover-throttle'\nexport * from './lib/server/password-reset-throttle'\nexport * from './lib/server/rate-limit-store'\nexport * from './lib/server/verify-email-cooldown'\nexport * from './lib/server/vercel-drain-signature'\nexport * from './lib/server/vercel-log-drain'\nexport * from './lib/server/visitor-write-rate-limit'\nexport * from './lib/server/console-api-rate-limit'\nexport * from './lib/server/realm-plugins'\nexport * from './lib/server/release-flags'\nexport * from './lib/server/resolve-people'\nexport * from './lib/server/member-photo'\nexport * from './lib/server/personal-data-export'\nexport * from './lib/server/media-delivery'\nexport * from './lib/server/media-download-tokens'\nexport * from './lib/server/media-quarantine'\nexport * from './lib/server/media-signing'\nexport * from './lib/server/media-storage-path'\nexport * from './lib/server/safe-equal'\nexport * from './lib/server/media-strong-digest'\nexport * from './lib/server/media-tombstone'\nexport * from './lib/server/media-variants'\nexport * from './lib/server/serve-media-cdn'\nexport * from './lib/server/serve-plugin-fetch'\nexport * from './lib/server/sso-break-glass-owners'\nexport * from './lib/server/sso-domain-policy'\nexport * from './lib/server/sso-enforcement'\nexport * from './lib/server/sso-provisioning'\nexport * from './lib/server/update-existing'\nexport * from './lib/server/contact-company-link'\nexport * from './lib/server/contact-email-index'\nexport * from './lib/server/contact-merge'\nexport * from './lib/server/upsert-contact'\n// The stage floor a won deal applies to its contact (AGL-2641).\nexport * from './lib/server/contact-lifecycle-floor'\nexport * from './lib/server/crm-records'\n// The `nextTaskAtMs` a contact, a company and a deal carry, recomputed from\n// their open tasks by every server-side task writer (AGL-2661).\nexport * from './lib/server/crm-next-activity'\n// Where a one-to-one email's activity row lives and how the delivery\n// webhook advances it (AGL-2615).\nexport * from './lib/server/crm-email-activity'\n// What a confirmed booking files on the CRM record — the meeting and the\n// follow-up (AGL-2660).\nexport * from './lib/server/crm-booking-activity'\n// The org's email capture token and the filing of one received message\n// on the record it was with (AGL-2657).\nexport * from './lib/server/crm-inbound-email'\n// A member's own addresses in a workspace, and the signed link that\n// confirms one (AGL-2975).\nexport * from './lib/server/member-email-aliases'\nexport * from './lib/server/user-profiles'\nexport * from './lib/server/legal-acceptance'\n// The person's decision about the platform's own product email, on their\n// document and on the operator's marketing contact (AGL-3185).\nexport * from './lib/server/platform-marketing-consent'\n"],"names":["domainProvider"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sCAAkC;AAChD,cAAc,2BAAuB;AACrC,cAAc,2BAAuB;AACrC,cAAc,iCAA6B;AAC3C,cAAc,6BAAyB;AACvC,cAAc,sCAAkC;AAChD,cAAc,yCAAqC;AACnD,cAAc,yCAAqC;AACnD,cAAc,sCAAkC;AAChD,cAAc,+BAA2B;AACzC,cAAc,kCAA8B;AAC5C,cAAc,4BAAwB;AACtC,cAAc,kCAA8B;AAC5C,cAAc,sCAAkC;AAChD,cAAc,sCAAkC;AAChD,cAAc,yCAAqC;AACnD,6EAA6E;AAC7E,+DAA+D;AAC/D,+EAA+E;AAC/E,wCAAwC;AACxC,SAASA,cAAc,QAAQ,kCAA8B;AAM7D,cAAc,wCAAoC;AAClD,cAAc,+BAA2B;AACzC,cAAc,gCAA4B;AAC1C,cAAc,oCAAgC;AAC9C,cAAc,sCAAkC;AAChD,cAAc,oCAAgC;AAC9C,4EAA4E;AAC5E,8EAA8E;AAC9E,sBAAsB;AACtB,cAAc,2CAAuC;AACrD,cAAc,yCAAqC;AACnD,cAAc,kCAA8B;AAC5C,wEAAwE;AACxE,4EAA4E;AAC5E,4EAA4E;AAC5E,8DAA8D;AAC9D,cAAc,uCAAmC;AACjD,cAAc,+BAA2B;AACzC,cAAc,8BAA0B;AACxC,cAAc,oCAAgC;AAC9C,cAAc,oCAAgC;AAC9C,cAAc,kCAA8B;AAC5C,cAAc,iCAA6B;AAC3C,4EAA4E;AAC5E,2EAA2E;AAC3E,wEAAwE;AACxE,cAAc,kDAA8C;AAC5D,cAAc,kCAA8B;AAC5C,cAAc,0CAAsC;AACpD,cAAc,wBAAoB;AAClC,cAAc,+BAA2B;AACzC,cAAc,2CAAuC;AACrD,cAAc,iCAA6B;AAC3C,cAAc,qCAAiC;AAC/C,cAAc,2CAAuC;AACrD,cAAc,mCAA+B;AAC7C,cAAc,uCAAmC;AACjD,cAAc,+BAA2B;AACzC,cAAc,2BAAuB;AACrC,cAAc,mCAA+B;AAC7C,cAAc,wCAAoC;AAClD,cAAc,oCAAgC;AAC9C,cAAc,gCAA4B;AAC1C,cAAc,8BAA0B;AACxC,cAAc,gCAA4B;AAC1C,cAAc,qCAAiC;AAC/C,cAAc,qCAAiC;AAC/C,cAAc,oCAAgC;AAC9C,cAAc,8CAA0C;AACxD,cAAc,0CAAsC;AACpD,cAAc,mCAA+B;AAC7C,cAAc,wCAAoC;AAClD,cAAc,yCAAqC;AACnD,cAAc,mCAA+B;AAC7C,cAAc,2CAAuC;AACrD,cAAc,yCAAqC;AACnD,cAAc,gCAA4B;AAC1C,cAAc,gCAA4B;AAC1C,cAAc,iCAA6B;AAC3C,cAAc,+BAA2B;AACzC,cAAc,uCAAmC;AACjD,cAAc,iCAA6B;AAC3C,cAAc,wCAAoC;AAClD,cAAc,mCAA+B;AAC7C,cAAc,gCAA4B;AAC1C,cAAc,qCAAiC;AAC/C,cAAc,6BAAyB;AACvC,cAAc,sCAAkC;AAChD,cAAc,kCAA8B;AAC5C,cAAc,iCAA6B;AAC3C,cAAc,kCAA8B;AAC5C,cAAc,qCAAiC;AAC/C,cAAc,yCAAqC;AACnD,cAAc,oCAAgC;AAC9C,cAAc,kCAA8B;AAC5C,cAAc,mCAA+B;AAC7C,cAAc,kCAA8B;AAC5C,cAAc,uCAAmC;AACjD,cAAc,sCAAkC;AAChD,cAAc,gCAA4B;AAC1C,cAAc,iCAA6B;AAC3C,gEAAgE;AAChE,cAAc,0CAAsC;AACpD,cAAc,8BAA0B;AACxC,4EAA4E;AAC5E,gEAAgE;AAChE,cAAc,oCAAgC;AAC9C,qEAAqE;AACrE,kCAAkC;AAClC,cAAc,qCAAiC;AAC/C,yEAAyE;AACzE,wBAAwB;AACxB,cAAc,uCAAmC;AACjD,uEAAuE;AACvE,wCAAwC;AACxC,cAAc,oCAAgC;AAC9C,oEAAoE;AACpE,2BAA2B;AAC3B,cAAc,uCAAmC;AACjD,cAAc,gCAA4B;AAC1C,cAAc,mCAA+B;AAC7C,yEAAyE;AACzE,+DAA+D;AAC/D,cAAc,6CAAyC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/data/admin/src/tenancy.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/server/analytics-retention'\nexport * from './lib/server/api-http'\nexport * from './lib/server/api-keys'\nexport * from './lib/server/account-emails'\nexport * from './lib/server/auth-pools'\nexport * from './lib/server/client-error-report'\nexport * from './lib/server/connect-account-status'\nexport * from './lib/server/connect-payout-failure'\nexport * from './lib/server/stripe-account-mode'\nexport * from './lib/server/auth-handoff'\nexport * from './lib/server/console-domains'\nexport * from './lib/server/dns-probe'\nexport * from './lib/server/sending-domains'\nexport * from './lib/server/host-sending-domain'\nexport * from './lib/server/sending-domain-debt'\nexport * from './lib/server/sending-domain-recheck'\n// The hosting seam itself, so a door can ask whether this deployment is able\n// to register a name at all before it promises the caller one.\n// `workspace-domains` re-exports the pieces its own callers share with it; the\n// provider behind it is not among them.\nexport { domainProvider } from './lib/server/domain-provider'\nexport type {\n DomainProvider,\n DomainProviderId,\n DomainScope,\n} from './lib/server/domain-provider'\nexport * from './lib/server/platform-domain-names'\nexport * from './lib/server/consume-once'\nexport * from './lib/server/csp-aggregate'\nexport * from './lib/server/data-storage-gate'\nexport * from './lib/server/contact-suppression'\nexport * from './lib/server/email-suppression'\n// The double opt-in quarantine, after the module whose keying it shares and\n// whose `filterTopicSendable` is the reader that makes it a quarantine rather\n// than a stored fact.\nexport * from './lib/server/email-topic-confirmation'\nexport * from './lib/server/email-unsubscribe-link'\nexport * from './lib/server/email-flow-gate'\n// Side-effecting on purpose, for the reason `email-send-rate` below is:\n// evaluating this module INSTALLS the marketing gate on `sendEmail`, so the\n// unsubscribe header, the suppression check and the frequency ceiling reach\n// every bulk sender without a call at each server entrypoint.\nexport * from './lib/server/email-marketing-gate'\nexport * from './lib/server/sms-keywords'\nexport * from './lib/server/document-id'\nexport * from './lib/server/collection-preview-token'\nexport * from './lib/server/edit-access-authz'\nexport * from './lib/server/edit-access-token'\nexport * from './lib/server/edit-hint-token'\nexport * from './lib/server/email-metering'\n// Side-effecting on purpose (AGL-2409): evaluating this module INSTALLS the\n// platform send-rate governor on `sendEmail`. Every server surface already\n// imports this barrel, so nothing has to remember to call an installer.\nexport * from './lib/server/campaign-conversion-attribution'\nexport * from './lib/server/email-send-rate'\nexport * from './lib/server/email-sender-reputation'\nexport * from './lib/server/erase'\nexport * from './lib/server/erase-person'\nexport * from './lib/server/dynamic-list-materialize'\nexport * from './lib/server/firebase-admin'\nexport * from './lib/server/free-workspace-cap'\nexport * from './lib/server/ga4-measurement-protocol'\nexport * from './lib/server/host-memberships'\nexport * from './lib/server/host-visitor-records'\nexport * from './lib/server/list-members'\nexport * from './lib/server/lockdown'\nexport * from './lib/server/token-revocation'\nexport * from './lib/server/tenant-write-lockdown'\nexport * from './lib/server/host-email-tokens'\nexport * from './lib/server/notifications'\nexport * from './lib/server/org-billing'\nexport * from './lib/server/organizations'\nexport * from './lib/server/duplicate-activity'\nexport * from './lib/server/duplicate-resource'\nexport * from './lib/server/workspace-domains'\nexport * from './lib/server/membership-recover-throttle'\nexport * from './lib/server/password-reset-throttle'\nexport * from './lib/server/rate-limit-store'\nexport * from './lib/server/verify-email-cooldown'\nexport * from './lib/server/vercel-drain-signature'\nexport * from './lib/server/vercel-log-drain'\nexport * from './lib/server/visitor-write-rate-limit'\nexport * from './lib/server/console-api-rate-limit'\nexport * from './lib/server/realm-plugins'\nexport * from './lib/server/release-flags'\nexport * from './lib/server/resolve-people'\nexport * from './lib/server/member-photo'\nexport * from './lib/server/personal-data-export'\nexport * from './lib/server/media-delivery'\nexport * from './lib/server/media-download-tokens'\nexport * from './lib/server/media-quarantine'\nexport * from './lib/server/media-signing'\nexport * from './lib/server/media-storage-path'\nexport * from './lib/server/safe-equal'\nexport * from './lib/server/media-strong-digest'\nexport * from './lib/server/media-tombstone'\nexport * from './lib/server/media-variants'\nexport * from './lib/server/serve-media-cdn'\nexport * from './lib/server/serve-plugin-fetch'\nexport * from './lib/server/sso-break-glass-owners'\nexport * from './lib/server/sso-domain-policy'\nexport * from './lib/server/sso-enforcement'\nexport * from './lib/server/sso-provisioning'\nexport * from './lib/server/update-existing'\nexport * from './lib/server/contact-company-link'\nexport * from './lib/server/contact-email-index'\nexport * from './lib/server/contact-merge'\nexport * from './lib/server/upsert-contact'\n// The stage floor a won deal applies to its contact (AGL-2641).\nexport * from './lib/server/contact-lifecycle-floor'\nexport * from './lib/server/crm-records'\n// The `nextTaskAtMs` a contact, a company and a deal carry, recomputed from\n// their open tasks by every server-side task writer (AGL-2661).\nexport * from './lib/server/crm-next-activity'\n// Where a one-to-one email's activity row lives and how the delivery\n// webhook advances it (AGL-2615).\nexport * from './lib/server/crm-email-activity'\n// What a confirmed booking files on the CRM record — the meeting and the\n// follow-up (AGL-2660).\nexport * from './lib/server/crm-booking-activity'\n// The org's email capture token and the filing of one received message\n// on the record it was with (AGL-2657).\nexport * from './lib/server/crm-inbound-email'\n// A member's own addresses in a workspace, and the signed link that\n// confirms one (AGL-2975).\nexport * from './lib/server/member-email-aliases'\nexport * from './lib/server/user-profiles'\nexport * from './lib/server/legal-acceptance'\n// The person's decision about the platform's own product email, on their\n// document and on the operator's marketing contact (AGL-3185).\nexport * from './lib/server/platform-marketing-consent'\n"],"names":["domainProvider"],"mappings":"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"}
|