-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathshare-link-service.ts
More file actions
152 lines (139 loc) · 5.72 KB
/
Copy pathshare-link-service.ts
File metadata and controls
152 lines (139 loc) · 5.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
/**
* @objectstack/spec/contracts/share-link-service
*
* Capability-token sharing — "anyone with the link" publication of a
* single record. Complementary to `ISharingService`, which models
* principal-based grants (share with specific users / teams / roles).
*
* Design notes:
*
* 1. **Opaque tokens.** A share link is a single random `token`
* (>=22 chars from a URL-safe alphabet). Anyone holding the
* token can fetch the record subject to `audience` and
* `expires_at`. Tokens are *not* derived from the record id;
* enumerating record ids reveals nothing.
*
* 2. **Per-object opt-in.** `createLink` refuses to mint a token
* for an object whose schema does not set `publicSharing.enabled`.
* The audience / permission requested must also lie within the
* object's `allowedAudiences` / `allowedPermissions` whitelist.
*
* 3. **Field-level redaction.** Each resolved link returns a
* `redactFields` set (object-default ∪ per-link). The REST gateway
* strips these before serialising to the response — the engine
* itself sees the raw row so existing hooks fire normally.
*
* 4. **No principal expansion.** Holders of a share token are NOT
* treated as principals in `sys_record_share`; the token authorises
* access only to its single `(object, recordId)` tuple at the
* declared `permission` level. This keeps audit trails clean and
* prevents lateral movement.
*/
/** Levels selectable when issuing a link. */
export type ShareLinkPermission = 'view' | 'comment' | 'edit';
/** Audience gating. */
export type ShareLinkAudience = 'public' | 'link_only' | 'signed_in' | 'email';
/** Persisted shape — mirrors `sys_share_link` columns 1:1. */
export interface ShareLink {
id: string;
/** Opaque URL-safe token. Always present; the only secret. */
token: string;
object_name: string;
record_id: string;
permission: ShareLinkPermission;
audience: ShareLinkAudience;
/** ISO timestamp; null means no expiry. */
expires_at?: string | null;
/** When `audience='email'`, the allow-listed addresses (lowercased). */
email_allowlist?: string[] | null;
/** Optional argon2/bcrypt hash; UI prompts for a password when set. */
password_hash?: string | null;
/** Per-link extra redactions, layered on top of the object-default set. */
redact_fields?: string[] | null;
/** Free-text shown in the share dialog (e.g. "Q3 contract for ACME"). */
label?: string | null;
/** When set, the link is revoked and `resolveToken` returns null. */
revoked_at?: string | null;
created_by?: string | null;
created_at?: string;
last_used_at?: string | null;
use_count?: number;
}
/** Input for {@link IShareLinkService.createLink}. */
export interface CreateShareLinkInput {
object: string;
recordId: string;
permission?: ShareLinkPermission;
audience?: ShareLinkAudience;
/** ISO timestamp or relative duration string ("7d", "24h") — service normalises. */
expiresAt?: string | null;
emailAllowlist?: string[];
/** Plain-text password; service hashes before persisting. */
password?: string;
/** Per-link redactions in addition to the object default. */
redactFields?: string[];
label?: string;
}
/** Filter for {@link IShareLinkService.listLinks}. */
export interface ListShareLinksFilter {
object?: string;
recordId?: string;
createdBy?: string;
includeRevoked?: boolean;
}
/** Outcome of resolving a token via the public endpoint. */
export interface ResolveShareLinkResult {
link: ShareLink;
/** Effective fields removed from the response (object default ∪ per-link). */
redactFields: string[];
}
/** Minimal context interface — kept compatible with `SharingExecutionContext`. */
export interface ShareLinkExecutionContext {
userId?: string;
tenantId?: string;
isSystem?: boolean;
/**
* [Finding-2] The caller's resolved positions / permissions. Populated by the
* verified route wiring so RLS-aware checks (e.g. "can this caller actually
* read the record being shared?") evaluate against the real principal rather
* than a spoofable header identity.
*/
positions?: string[];
permissions?: string[];
}
/**
* Default implementation lives in `@objectstack/plugin-sharing`.
*
* Implementations MUST treat `context.isSystem === true` as a bypass
* (skip the per-object opt-in check) so platform bootstrappers can seed
* demo links.
*/
export interface IShareLinkService {
/** Mint a new link. Throws when the object is not opt-in or limits are exceeded. */
createLink(input: CreateShareLinkInput, context: ShareLinkExecutionContext): Promise<ShareLink>;
/** Mark a link as revoked. No-op when already revoked or not found. */
revokeLink(idOrToken: string, context: ShareLinkExecutionContext): Promise<void>;
/** List links for a record, an object, or a creator. */
listLinks(filter: ListShareLinksFilter, context: ShareLinkExecutionContext): Promise<ShareLink[]>;
/**
* Resolve a token at request-handling time. Returns null when the
* token does not exist, is revoked, expired, or fails the audience
* check. Increments `use_count` / `last_used_at` as a side effect.
*
* @param token raw token from the URL / cookie
* @param probe contextual gates the caller has already evaluated
* (e.g. signed-in user, recipient email, supplied
* password)
*/
resolveToken(
token: string,
probe?: {
signedInUserId?: string;
recipientEmail?: string;
providedPassword?: string;
},
): Promise<ResolveShareLinkResult | null>;
}
/** Service-registry key — keep in sync with the SharingPlugin registration. */
export const SHARE_LINK_SERVICE = 'shareLinks' as const;