@objectstack/plugin-sharing 7.3.0 → 7.4.1
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +92 -0
- package/dist/index.d.mts +9703 -2
- package/dist/index.d.ts +9703 -2
- package/dist/index.js +1685 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1690 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/scripts/i18n-extract.config.ts +31 -0
- package/src/index.ts +1 -1
- package/src/objects/index.ts +14 -0
- package/src/objects/sys-record-share.object.ts +229 -0
- package/src/objects/sys-share-link.object.ts +255 -0
- package/src/objects/sys-sharing-rule.object.ts +190 -0
- package/src/sharing-plugin.ts +31 -1
- package/src/translations/en.objects.generated.ts +275 -0
- package/src/translations/es-ES.objects.generated.ts +275 -0
- package/src/translations/index.ts +23 -0
- package/src/translations/ja-JP.objects.generated.ts +275 -0
- package/src/translations/zh-CN.objects.generated.ts +275 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-sharing",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "7.
|
|
17
|
-
"@objectstack/objectql": "7.
|
|
18
|
-
"@objectstack/platform-objects": "7.
|
|
19
|
-
"@objectstack/spec": "7.
|
|
16
|
+
"@objectstack/core": "7.4.1",
|
|
17
|
+
"@objectstack/objectql": "7.4.1",
|
|
18
|
+
"@objectstack/platform-objects": "7.4.1",
|
|
19
|
+
"@objectstack/spec": "7.4.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^25.9.1",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build-time only config for `os i18n extract` (ADR-0029 D8). Not deployed.
|
|
5
|
+
* The plugin owns the i18n extraction for the objects it owns; the
|
|
6
|
+
* `translations` baseline is this plugin's OWN generated bundles so re-running
|
|
7
|
+
* `--merge` preserves every hand-translated string. (Initial zh-CN/ja-JP/es-ES
|
|
8
|
+
* strings were seeded from @objectstack/platform-objects.)
|
|
9
|
+
*
|
|
10
|
+
* os i18n extract packages/plugins/plugin-sharing/scripts/i18n-extract.config.ts \
|
|
11
|
+
* --locales=zh-CN,ja-JP,es-ES --fill=default --objects-only \
|
|
12
|
+
* --out=packages/plugins/plugin-sharing/src/translations
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { defineStack } from '@objectstack/spec';
|
|
16
|
+
import { SysRecordShare, SysSharingRule, SysShareLink } from '../src/objects/index.js';
|
|
17
|
+
import { enObjects } from '../src/translations/en.objects.generated.js';
|
|
18
|
+
import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js';
|
|
19
|
+
import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js';
|
|
20
|
+
import { esESObjects } from '../src/translations/es-ES.objects.generated.js';
|
|
21
|
+
|
|
22
|
+
export default defineStack({
|
|
23
|
+
name: 'plugin-sharing-i18n-extract',
|
|
24
|
+
objects: [SysRecordShare, SysSharingRule, SysShareLink] as any,
|
|
25
|
+
translations: [
|
|
26
|
+
{ en: { objects: enObjects } },
|
|
27
|
+
{ 'zh-CN': { objects: zhCNObjects } },
|
|
28
|
+
{ 'ja-JP': { objects: jaJPObjects } },
|
|
29
|
+
{ 'es-ES': { objects: esESObjects } },
|
|
30
|
+
],
|
|
31
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* authenticated execution context.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
export { SysRecordShare, SysSharingRule, SysShareLink } from '
|
|
12
|
+
export { SysRecordShare, SysSharingRule, SysShareLink } from './objects/index.js';
|
|
13
13
|
export { SysDepartment, SysDepartmentMember } from '@objectstack/platform-objects/identity';
|
|
14
14
|
export {
|
|
15
15
|
SharingService,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sharing objects owned by `@objectstack/plugin-sharing` (ADR-0029 K2).
|
|
5
|
+
*
|
|
6
|
+
* Moved here from the `@objectstack/platform-objects` monolith so the plugin
|
|
7
|
+
* owns its data model, behavior, and admin menu as one unit. The RBAC objects
|
|
8
|
+
* (role / permission-set / *-permission-set) live in
|
|
9
|
+
* `@objectstack/plugin-security`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { SysRecordShare } from './sys-record-share.object.js';
|
|
13
|
+
export { SysSharingRule } from './sys-sharing-rule.object.js';
|
|
14
|
+
export { SysShareLink } from './sys-share-link.object.js';
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* sys_record_share — Per-Record Sharing Grant
|
|
7
|
+
*
|
|
8
|
+
* Bridges the ownership-only baseline established by `object.sharingModel`
|
|
9
|
+
* with the real-world need to delegate access to a single record. Each
|
|
10
|
+
* row says: "principal P has access level L on (object O, record R),
|
|
11
|
+
* because of source S (manual grant or rule)."
|
|
12
|
+
*
|
|
13
|
+
* Enforcement lives in `@objectstack/plugin-sharing`:
|
|
14
|
+
* - For objects with `sharingModel: 'private'`, the engine middleware
|
|
15
|
+
* AND-s `{$or:[{owner_id:userId},{id:{$in:[grantedRecordIds]}}]}`
|
|
16
|
+
* into every `find` against that object.
|
|
17
|
+
* - For objects with `sharingModel: 'private' | 'read'`, the same
|
|
18
|
+
* middleware enforces edit/delete by checking ownership OR a share
|
|
19
|
+
* row with `access_level in ('edit','full')`.
|
|
20
|
+
*
|
|
21
|
+
* Conventions:
|
|
22
|
+
* - `object_name` is the short object name (e.g. `account`, `lead`).
|
|
23
|
+
* - `recipient_type` mirrors `ShareRecipientType` from the spec
|
|
24
|
+
* (`user` is enforced today; `group`/`role` are persisted for
|
|
25
|
+
* forward-compatibility).
|
|
26
|
+
* - `source = 'manual'` rows are created by a user via the REST
|
|
27
|
+
* `POST /data/:object/:id/shares` endpoint. `source = 'rule'` rows
|
|
28
|
+
* are materialised by the sharing-rule evaluator (future); the
|
|
29
|
+
* `source_id` lets the evaluator reconcile stale grants.
|
|
30
|
+
*
|
|
31
|
+
* @namespace sys
|
|
32
|
+
*/
|
|
33
|
+
export const SysRecordShare = ObjectSchema.create({
|
|
34
|
+
name: 'sys_record_share',
|
|
35
|
+
label: 'Record Share',
|
|
36
|
+
pluralLabel: 'Record Shares',
|
|
37
|
+
icon: 'share',
|
|
38
|
+
isSystem: true,
|
|
39
|
+
managedBy: 'system',
|
|
40
|
+
description: 'Per-record sharing grant — extends OWD with explicit access',
|
|
41
|
+
titleFormat: '{object_name}/{record_id} → {recipient_id} ({access_level})',
|
|
42
|
+
compactLayout: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source'],
|
|
43
|
+
|
|
44
|
+
listViews: {
|
|
45
|
+
granted_to_me: {
|
|
46
|
+
type: 'grid',
|
|
47
|
+
name: 'granted_to_me',
|
|
48
|
+
label: 'Granted to Me',
|
|
49
|
+
data: { provider: 'object', object: 'sys_record_share' },
|
|
50
|
+
columns: ['object_name', 'record_id', 'access_level', 'source', 'granted_by', 'created_at'],
|
|
51
|
+
filter: [
|
|
52
|
+
{ field: 'recipient_type', operator: 'equals', value: 'user' },
|
|
53
|
+
{ field: 'recipient_id', operator: 'equals', value: '{current_user_id}' },
|
|
54
|
+
],
|
|
55
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
56
|
+
pagination: { pageSize: 50 },
|
|
57
|
+
},
|
|
58
|
+
granted_by_me: {
|
|
59
|
+
type: 'grid',
|
|
60
|
+
name: 'granted_by_me',
|
|
61
|
+
label: 'Granted by Me',
|
|
62
|
+
data: { provider: 'object', object: 'sys_record_share' },
|
|
63
|
+
columns: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source', 'created_at'],
|
|
64
|
+
filter: [
|
|
65
|
+
{ field: 'granted_by', operator: 'equals', value: '{current_user_id}' },
|
|
66
|
+
],
|
|
67
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
68
|
+
pagination: { pageSize: 50 },
|
|
69
|
+
},
|
|
70
|
+
by_object: {
|
|
71
|
+
type: 'grid',
|
|
72
|
+
name: 'by_object',
|
|
73
|
+
label: 'By Object',
|
|
74
|
+
data: { provider: 'object', object: 'sys_record_share' },
|
|
75
|
+
columns: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source', 'created_at'],
|
|
76
|
+
sort: [{ field: 'object_name', order: 'asc' }, { field: 'created_at', order: 'desc' }],
|
|
77
|
+
grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] },
|
|
78
|
+
pagination: { pageSize: 100 },
|
|
79
|
+
},
|
|
80
|
+
manual_grants: {
|
|
81
|
+
type: 'grid',
|
|
82
|
+
name: 'manual_grants',
|
|
83
|
+
label: 'Manual Grants',
|
|
84
|
+
data: { provider: 'object', object: 'sys_record_share' },
|
|
85
|
+
columns: ['object_name', 'record_id', 'recipient_id', 'access_level', 'granted_by', 'reason', 'created_at'],
|
|
86
|
+
filter: [{ field: 'source', operator: 'equals', value: 'manual' }],
|
|
87
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
88
|
+
pagination: { pageSize: 50 },
|
|
89
|
+
},
|
|
90
|
+
rule_grants: {
|
|
91
|
+
type: 'grid',
|
|
92
|
+
name: 'rule_grants',
|
|
93
|
+
label: 'Rule Grants',
|
|
94
|
+
data: { provider: 'object', object: 'sys_record_share' },
|
|
95
|
+
columns: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source_id', 'created_at'],
|
|
96
|
+
filter: [{ field: 'source', operator: 'in', value: ['rule', 'team', 'inherited'] }],
|
|
97
|
+
sort: [{ field: 'source_id', order: 'asc' }, { field: 'created_at', order: 'desc' }],
|
|
98
|
+
pagination: { pageSize: 50 },
|
|
99
|
+
},
|
|
100
|
+
all_shares: {
|
|
101
|
+
type: 'grid',
|
|
102
|
+
name: 'all_shares',
|
|
103
|
+
label: 'All',
|
|
104
|
+
data: { provider: 'object', object: 'sys_record_share' },
|
|
105
|
+
columns: ['object_name', 'record_id', 'recipient_type', 'recipient_id', 'access_level', 'source', 'created_at'],
|
|
106
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
107
|
+
pagination: { pageSize: 100 },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
fields: {
|
|
112
|
+
id: Field.text({
|
|
113
|
+
label: 'Share ID',
|
|
114
|
+
required: true,
|
|
115
|
+
readonly: true,
|
|
116
|
+
group: 'System',
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
// ── Target (which record is being shared) ────────────────────
|
|
120
|
+
object_name: Field.text({
|
|
121
|
+
label: 'Object',
|
|
122
|
+
required: true,
|
|
123
|
+
maxLength: 100,
|
|
124
|
+
description: 'Short object name of the shared record',
|
|
125
|
+
group: 'Target',
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
record_id: Field.text({
|
|
129
|
+
label: 'Record',
|
|
130
|
+
required: true,
|
|
131
|
+
maxLength: 100,
|
|
132
|
+
description: 'Primary key of the shared record within object_name',
|
|
133
|
+
group: 'Target',
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
// ── Recipient (who receives access) ──────────────────────────
|
|
137
|
+
recipient_type: Field.select(
|
|
138
|
+
['user', 'group', 'role', 'role_and_subordinates', 'guest'],
|
|
139
|
+
{
|
|
140
|
+
label: 'Recipient Type',
|
|
141
|
+
required: true,
|
|
142
|
+
defaultValue: 'user',
|
|
143
|
+
description: 'Kind of principal that holds the grant',
|
|
144
|
+
group: 'Recipient',
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
|
|
148
|
+
recipient_id: Field.text({
|
|
149
|
+
label: 'Recipient',
|
|
150
|
+
required: true,
|
|
151
|
+
maxLength: 100,
|
|
152
|
+
description: 'ID of the user/group/role that receives access',
|
|
153
|
+
group: 'Recipient',
|
|
154
|
+
}),
|
|
155
|
+
|
|
156
|
+
access_level: Field.select(
|
|
157
|
+
['read', 'edit', 'full'],
|
|
158
|
+
{
|
|
159
|
+
label: 'Access Level',
|
|
160
|
+
required: true,
|
|
161
|
+
defaultValue: 'read',
|
|
162
|
+
description: 'What the recipient can do — read | edit | full (transfer/share/delete)',
|
|
163
|
+
group: 'Recipient',
|
|
164
|
+
},
|
|
165
|
+
),
|
|
166
|
+
|
|
167
|
+
// ── Provenance ───────────────────────────────────────────────
|
|
168
|
+
source: Field.select(
|
|
169
|
+
['manual', 'rule', 'team', 'inherited'],
|
|
170
|
+
{
|
|
171
|
+
label: 'Source',
|
|
172
|
+
required: true,
|
|
173
|
+
defaultValue: 'manual',
|
|
174
|
+
description: 'Why this grant exists — used by the rule evaluator to reconcile',
|
|
175
|
+
group: 'Provenance',
|
|
176
|
+
},
|
|
177
|
+
),
|
|
178
|
+
|
|
179
|
+
source_id: Field.text({
|
|
180
|
+
label: 'Source ID',
|
|
181
|
+
required: false,
|
|
182
|
+
maxLength: 200,
|
|
183
|
+
description: 'Rule name / team id when source != manual',
|
|
184
|
+
group: 'Provenance',
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
granted_by: Field.lookup('sys_user', {
|
|
188
|
+
label: 'Granted By',
|
|
189
|
+
required: false,
|
|
190
|
+
description: 'User that created the grant (manual only)',
|
|
191
|
+
group: 'Provenance',
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
reason: Field.text({
|
|
195
|
+
label: 'Reason',
|
|
196
|
+
required: false,
|
|
197
|
+
maxLength: 500,
|
|
198
|
+
description: 'Optional free-text explanation surfaced to the recipient',
|
|
199
|
+
group: 'Provenance',
|
|
200
|
+
}),
|
|
201
|
+
|
|
202
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
203
|
+
created_at: Field.datetime({
|
|
204
|
+
label: 'Created At',
|
|
205
|
+
required: true,
|
|
206
|
+
defaultValue: 'NOW()',
|
|
207
|
+
readonly: true,
|
|
208
|
+
group: 'System',
|
|
209
|
+
}),
|
|
210
|
+
|
|
211
|
+
updated_at: Field.datetime({
|
|
212
|
+
label: 'Updated At',
|
|
213
|
+
required: false,
|
|
214
|
+
group: 'System',
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
indexes: [
|
|
219
|
+
// Hot path: "all records visible to user U on object O" — the
|
|
220
|
+
// middleware reads (object_name, recipient_type, recipient_id) to
|
|
221
|
+
// build the `id IN (...)` predicate on every find.
|
|
222
|
+
{ fields: ['object_name', 'recipient_type', 'recipient_id'] },
|
|
223
|
+
// "all grants on this record" — used by the share-management UI
|
|
224
|
+
// and by canEdit() to look up explicit grants.
|
|
225
|
+
{ fields: ['object_name', 'record_id'] },
|
|
226
|
+
// Reconciliation key for rule-driven shares.
|
|
227
|
+
{ fields: ['source', 'source_id'] },
|
|
228
|
+
],
|
|
229
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* sys_share_link — Capability-Token Public Share Links
|
|
7
|
+
*
|
|
8
|
+
* Each row authorises read (or write) access to ONE record of ONE
|
|
9
|
+
* object via an opaque URL-safe token. Complements `sys_record_share`,
|
|
10
|
+
* which models principal-based grants (share with a specific user /
|
|
11
|
+
* team / role). A single record may have rows in both tables; the
|
|
12
|
+
* union determines effective access.
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle:
|
|
15
|
+
*
|
|
16
|
+
* 1. `IShareLinkService.createLink` validates the request against the
|
|
17
|
+
* target object's `publicSharing` whitelist and inserts a row.
|
|
18
|
+
* Token is a 24-char URL-safe random string.
|
|
19
|
+
*
|
|
20
|
+
* 2. `IShareLinkService.resolveToken` (called from the public
|
|
21
|
+
* `/api/v1/share-links/:token` middleware on every request)
|
|
22
|
+
* verifies the row is not revoked / not expired, applies audience
|
|
23
|
+
* / password gates, increments `use_count` + `last_used_at`, and
|
|
24
|
+
* returns the effective redaction set.
|
|
25
|
+
*
|
|
26
|
+
* 3. `IShareLinkService.revokeLink` stamps `revoked_at`. Rows are
|
|
27
|
+
* preserved for audit; resolveToken returns null after revocation.
|
|
28
|
+
*
|
|
29
|
+
* Conventions:
|
|
30
|
+
* - `object_name` is the short object name (`account`, `ai_conversation`, …)
|
|
31
|
+
* - `record_id` is the primary key of the target record within object_name
|
|
32
|
+
* - `audience` mirrors `ShareLinkAudience` in spec/contracts; the
|
|
33
|
+
* middleware enforces additional gating per audience
|
|
34
|
+
* - `redact_fields` overlays on top of the schema-default redaction
|
|
35
|
+
* set declared on `object.publicSharing.redactFields`
|
|
36
|
+
*
|
|
37
|
+
* managedBy: 'system' — admins inspect via the audit grid but all
|
|
38
|
+
* writes flow through `IShareLinkService` so the per-object opt-in,
|
|
39
|
+
* expiry caps, and audit hooks fire.
|
|
40
|
+
*
|
|
41
|
+
* @namespace sys
|
|
42
|
+
*/
|
|
43
|
+
export const SysShareLink = ObjectSchema.create({
|
|
44
|
+
name: 'sys_share_link',
|
|
45
|
+
label: 'Share Link',
|
|
46
|
+
pluralLabel: 'Share Links',
|
|
47
|
+
icon: 'link-2',
|
|
48
|
+
isSystem: true,
|
|
49
|
+
managedBy: 'system',
|
|
50
|
+
description: 'Opaque capability token granting access to a single record. Notion/Figma-style public link sharing.',
|
|
51
|
+
titleFormat: '{object_name}/{record_id} ({permission})',
|
|
52
|
+
compactLayout: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'revoked_at'],
|
|
53
|
+
|
|
54
|
+
listViews: {
|
|
55
|
+
active_links: {
|
|
56
|
+
type: 'grid',
|
|
57
|
+
name: 'active_links',
|
|
58
|
+
label: 'Active',
|
|
59
|
+
data: { provider: 'object', object: 'sys_share_link' },
|
|
60
|
+
columns: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'use_count', 'last_used_at'],
|
|
61
|
+
filter: [{ field: 'revoked_at', operator: 'isNull' }],
|
|
62
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
63
|
+
pagination: { pageSize: 100 },
|
|
64
|
+
},
|
|
65
|
+
by_me: {
|
|
66
|
+
type: 'grid',
|
|
67
|
+
name: 'by_me',
|
|
68
|
+
label: 'Created by Me',
|
|
69
|
+
data: { provider: 'object', object: 'sys_share_link' },
|
|
70
|
+
columns: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'revoked_at'],
|
|
71
|
+
filter: [{ field: 'created_by', operator: 'equals', value: '{current_user_id}' }],
|
|
72
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
73
|
+
pagination: { pageSize: 100 },
|
|
74
|
+
},
|
|
75
|
+
revoked: {
|
|
76
|
+
type: 'grid',
|
|
77
|
+
name: 'revoked',
|
|
78
|
+
label: 'Revoked',
|
|
79
|
+
data: { provider: 'object', object: 'sys_share_link' },
|
|
80
|
+
columns: ['object_name', 'record_id', 'revoked_at', 'created_by'],
|
|
81
|
+
filter: [{ field: 'revoked_at', operator: 'isNotNull' }],
|
|
82
|
+
sort: [{ field: 'revoked_at', order: 'desc' }],
|
|
83
|
+
pagination: { pageSize: 50 },
|
|
84
|
+
},
|
|
85
|
+
all_links: {
|
|
86
|
+
type: 'grid',
|
|
87
|
+
name: 'all_links',
|
|
88
|
+
label: 'All',
|
|
89
|
+
data: { provider: 'object', object: 'sys_share_link' },
|
|
90
|
+
columns: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'revoked_at', 'created_at'],
|
|
91
|
+
sort: [{ field: 'created_at', order: 'desc' }],
|
|
92
|
+
pagination: { pageSize: 200 },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
fields: {
|
|
97
|
+
id: Field.text({
|
|
98
|
+
label: 'Link ID',
|
|
99
|
+
required: true,
|
|
100
|
+
readonly: true,
|
|
101
|
+
group: 'System',
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
// ── Token (the secret) ───────────────────────────────────────
|
|
105
|
+
token: Field.text({
|
|
106
|
+
label: 'Token',
|
|
107
|
+
required: true,
|
|
108
|
+
maxLength: 64,
|
|
109
|
+
description: 'Opaque URL-safe random token (≥ 22 chars). The only secret in this row.',
|
|
110
|
+
group: 'Token',
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
// ── Target ───────────────────────────────────────────────────
|
|
114
|
+
object_name: Field.text({
|
|
115
|
+
label: 'Object',
|
|
116
|
+
required: true,
|
|
117
|
+
maxLength: 100,
|
|
118
|
+
description: 'Short object name of the shared record (e.g. ai_conversation, contracts_contract)',
|
|
119
|
+
group: 'Target',
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
record_id: Field.text({
|
|
123
|
+
label: 'Record',
|
|
124
|
+
required: true,
|
|
125
|
+
maxLength: 100,
|
|
126
|
+
description: 'Primary key of the shared record within object_name',
|
|
127
|
+
group: 'Target',
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
// ── Access Policy ────────────────────────────────────────────
|
|
131
|
+
permission: Field.select(
|
|
132
|
+
[
|
|
133
|
+
{ label: 'View', value: 'view' },
|
|
134
|
+
{ label: 'Comment', value: 'comment' },
|
|
135
|
+
{ label: 'Edit', value: 'edit' },
|
|
136
|
+
],
|
|
137
|
+
{
|
|
138
|
+
label: 'Permission',
|
|
139
|
+
required: true,
|
|
140
|
+
defaultValue: 'view',
|
|
141
|
+
description: 'What the link holder can do with the record',
|
|
142
|
+
group: 'Access Policy',
|
|
143
|
+
},
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
audience: Field.select(
|
|
147
|
+
[
|
|
148
|
+
{ label: 'Public (indexable)', value: 'public' },
|
|
149
|
+
{ label: 'Anyone with the link', value: 'link_only' },
|
|
150
|
+
{ label: 'Signed-in users', value: 'signed_in' },
|
|
151
|
+
{ label: 'Specific emails', value: 'email' },
|
|
152
|
+
],
|
|
153
|
+
{
|
|
154
|
+
label: 'Audience',
|
|
155
|
+
required: true,
|
|
156
|
+
defaultValue: 'link_only',
|
|
157
|
+
description: 'Gating layer applied on top of the token check',
|
|
158
|
+
group: 'Access Policy',
|
|
159
|
+
},
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
expires_at: Field.datetime({
|
|
163
|
+
label: 'Expires At',
|
|
164
|
+
description: 'When set, resolveToken returns null after this timestamp',
|
|
165
|
+
group: 'Access Policy',
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
email_allowlist: Field.json({
|
|
169
|
+
label: 'Email Allowlist',
|
|
170
|
+
description: 'Lowercased addresses checked when audience=email',
|
|
171
|
+
group: 'Access Policy',
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
password_hash: Field.text({
|
|
175
|
+
label: 'Password Hash',
|
|
176
|
+
maxLength: 256,
|
|
177
|
+
description: 'Argon2/bcrypt hash. When set, the UI prompts for a password before rendering.',
|
|
178
|
+
group: 'Access Policy',
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
redact_fields: Field.json({
|
|
182
|
+
label: 'Per-Link Redactions',
|
|
183
|
+
description: 'Extra fields stripped from the response, on top of the object-default set',
|
|
184
|
+
group: 'Access Policy',
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
label: Field.text({
|
|
188
|
+
label: 'Label',
|
|
189
|
+
maxLength: 200,
|
|
190
|
+
description: 'Free-text shown in the share dialog (e.g. "ACME Q3 contract")',
|
|
191
|
+
group: 'Metadata',
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
195
|
+
revoked_at: Field.datetime({
|
|
196
|
+
label: 'Revoked At',
|
|
197
|
+
readonly: true,
|
|
198
|
+
description: 'When set, the link is permanently disabled',
|
|
199
|
+
group: 'Lifecycle',
|
|
200
|
+
}),
|
|
201
|
+
|
|
202
|
+
created_by: Field.lookup('sys_user', {
|
|
203
|
+
label: 'Created By',
|
|
204
|
+
readonly: true,
|
|
205
|
+
description: 'Issuer of the link',
|
|
206
|
+
group: 'Lifecycle',
|
|
207
|
+
}),
|
|
208
|
+
|
|
209
|
+
created_at: Field.datetime({
|
|
210
|
+
label: 'Created At',
|
|
211
|
+
required: true,
|
|
212
|
+
defaultValue: 'NOW()',
|
|
213
|
+
readonly: true,
|
|
214
|
+
group: 'Lifecycle',
|
|
215
|
+
}),
|
|
216
|
+
|
|
217
|
+
last_used_at: Field.datetime({
|
|
218
|
+
label: 'Last Used At',
|
|
219
|
+
readonly: true,
|
|
220
|
+
description: 'Stamped by resolveToken; used by the dashboard to highlight active links',
|
|
221
|
+
group: 'Lifecycle',
|
|
222
|
+
}),
|
|
223
|
+
|
|
224
|
+
use_count: Field.number({
|
|
225
|
+
label: 'Use Count',
|
|
226
|
+
defaultValue: 0,
|
|
227
|
+
readonly: true,
|
|
228
|
+
description: 'Incremented by resolveToken on every successful resolution',
|
|
229
|
+
group: 'Lifecycle',
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
indexes: [
|
|
234
|
+
// Hot path: resolveToken — one row lookup per public request.
|
|
235
|
+
{ fields: ['token'], unique: true },
|
|
236
|
+
// Management UI: "all links for this record".
|
|
237
|
+
{ fields: ['object_name', 'record_id'] },
|
|
238
|
+
// "Active links I issued".
|
|
239
|
+
{ fields: ['created_by', 'revoked_at'] },
|
|
240
|
+
// Reaper for expired rows (background sweep).
|
|
241
|
+
{ fields: ['expires_at'] },
|
|
242
|
+
],
|
|
243
|
+
|
|
244
|
+
enable: {
|
|
245
|
+
trackHistory: false,
|
|
246
|
+
searchable: false,
|
|
247
|
+
apiEnabled: true,
|
|
248
|
+
// The /api/v1/share-links endpoints are the authoritative surface;
|
|
249
|
+
// the generic data API is exposed read-only for the admin grid.
|
|
250
|
+
apiMethods: ['get', 'list'],
|
|
251
|
+
trash: false,
|
|
252
|
+
mru: false,
|
|
253
|
+
clone: false,
|
|
254
|
+
},
|
|
255
|
+
});
|