@pylonsync/sdk 0.3.158 → 0.3.161
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/package.json +1 -1
- package/src/index.ts +87 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -50,6 +50,52 @@ export interface FieldDefinition {
|
|
|
50
50
|
/** CRDT container override. Omitted entirely for the default
|
|
51
51
|
* (LWW for scalars, LoroText for richtext). */
|
|
52
52
|
crdt?: CrdtAnnotation;
|
|
53
|
+
/**
|
|
54
|
+
* When true, the field is **never serialized in HTTP responses**.
|
|
55
|
+
* Use for secrets / billing-side identity / hashes that the server
|
|
56
|
+
* needs to read internally but should never leak to clients.
|
|
57
|
+
*
|
|
58
|
+
* Stripped at every public read boundary:
|
|
59
|
+
* - `GET /api/entities/<entity>` (list)
|
|
60
|
+
* - `GET /api/entities/<entity>/<id>` (single row)
|
|
61
|
+
* - `GET /api/auth/session` (User row projection)
|
|
62
|
+
* - Sync push deltas
|
|
63
|
+
*
|
|
64
|
+
* Still readable from inside server functions via `ctx.db.*` —
|
|
65
|
+
* the framework trusts your handler logic to decide what to
|
|
66
|
+
* return. If you pass the row through unmodified to the client,
|
|
67
|
+
* the field IS still stripped at the function-response boundary,
|
|
68
|
+
* provided the value is a plain row from `ctx.db.get` (which
|
|
69
|
+
* tags it with the entity name so the boundary knows what to
|
|
70
|
+
* filter).
|
|
71
|
+
*
|
|
72
|
+
* `passwordHash` on every User entity is implicitly serverOnly
|
|
73
|
+
* even without this annotation, by the framework's hardcoded
|
|
74
|
+
* convention. New apps should still mark it explicitly so the
|
|
75
|
+
* intent shows up in the schema.
|
|
76
|
+
*/
|
|
77
|
+
serverOnly?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* When true, the field is **set on insert but cannot be changed
|
|
80
|
+
* by client updates**. The framework rejects any `PATCH`/`PUT`
|
|
81
|
+
* payload that mentions the field with a `READONLY_FIELD` error,
|
|
82
|
+
* before policy evaluation. Admin contexts bypass this check (as
|
|
83
|
+
* with all other framework gates), so migrations + ops scripts
|
|
84
|
+
* can still rewrite owner-shaped fields.
|
|
85
|
+
*
|
|
86
|
+
* Use for identity-shaped columns that need to be settable at
|
|
87
|
+
* creation but immutable after — `authorId`, `orgId`,
|
|
88
|
+
* `createdBy`, `stripeCustomerId`. Closes the canonical IDOR
|
|
89
|
+
* shape where a policy gates on `data.authorId == auth.userId`
|
|
90
|
+
* but the attacker passes a different `authorId` in the update
|
|
91
|
+
* payload to flip the row's ownership.
|
|
92
|
+
*
|
|
93
|
+
* Server-side writes (via `ctx.db.update` inside a function)
|
|
94
|
+
* still go through — readonly only blocks the HTTP entity
|
|
95
|
+
* routes (`PATCH /api/entities/<entity>/<id>`) and `/api/transact`.
|
|
96
|
+
* Server code is trusted to enforce its own invariants.
|
|
97
|
+
*/
|
|
98
|
+
readonly?: boolean;
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
interface FieldBuilder {
|
|
@@ -66,6 +112,25 @@ interface FieldBuilder {
|
|
|
66
112
|
* concurrently merge cleanly instead of last-write-wins.
|
|
67
113
|
*/
|
|
68
114
|
crdt(annotation: CrdtAnnotation): FieldBuilder;
|
|
115
|
+
/**
|
|
116
|
+
* Mark the field as never-returned in HTTP responses. See
|
|
117
|
+
* [`FieldDefinition.serverOnly`] for the full semantics.
|
|
118
|
+
*
|
|
119
|
+
* Example: `stripeCustomerId: field.string().serverOnly()` keeps
|
|
120
|
+
* the Stripe customer id out of `/api/entities/Org/<id>` responses
|
|
121
|
+
* while staying readable from `ctx.db.get` inside the
|
|
122
|
+
* stripeWebhook action.
|
|
123
|
+
*/
|
|
124
|
+
serverOnly(): FieldBuilder;
|
|
125
|
+
/**
|
|
126
|
+
* Mark the field as set-on-insert-only. See [`FieldDefinition.readonly`]
|
|
127
|
+
* for the full semantics.
|
|
128
|
+
*
|
|
129
|
+
* Example: `authorId: field.id("User").readonly()` lets the framework
|
|
130
|
+
* reject any `PATCH` payload trying to rewrite the row's author —
|
|
131
|
+
* closes the IDOR-via-update-payload class.
|
|
132
|
+
*/
|
|
133
|
+
readonly(): FieldBuilder;
|
|
69
134
|
}
|
|
70
135
|
|
|
71
136
|
function createFieldBuilder(type: FieldType): FieldBuilder {
|
|
@@ -84,6 +149,12 @@ function buildField(def: FieldDefinition): FieldBuilder {
|
|
|
84
149
|
crdt(annotation) {
|
|
85
150
|
return buildField({ ...def, crdt: annotation });
|
|
86
151
|
},
|
|
152
|
+
serverOnly() {
|
|
153
|
+
return buildField({ ...def, serverOnly: true });
|
|
154
|
+
},
|
|
155
|
+
readonly() {
|
|
156
|
+
return buildField({ ...def, readonly: true });
|
|
157
|
+
},
|
|
87
158
|
};
|
|
88
159
|
}
|
|
89
160
|
|
|
@@ -312,6 +383,13 @@ export interface ManifestField {
|
|
|
312
383
|
/** CRDT container override; matches `pylon_kernel::CrdtAnnotation` on
|
|
313
384
|
* the Rust side. Omitted entirely when the field uses the default. */
|
|
314
385
|
crdt?: CrdtAnnotation;
|
|
386
|
+
/** Set when the field is `field.X().serverOnly()` — see
|
|
387
|
+
* [`FieldDefinition.serverOnly`]. Omitted by default so JSON
|
|
388
|
+
* manifests stay compact for unannotated apps. */
|
|
389
|
+
serverOnly?: boolean;
|
|
390
|
+
/** Set when the field is `field.X().readonly()` — see
|
|
391
|
+
* [`FieldDefinition.readonly`]. Omitted by default. */
|
|
392
|
+
readonly?: boolean;
|
|
315
393
|
}
|
|
316
394
|
|
|
317
395
|
export interface ManifestIndex {
|
|
@@ -409,11 +487,18 @@ export function entitiesToManifest(
|
|
|
409
487
|
optional: fb._def.optional,
|
|
410
488
|
unique: fb._def.unique,
|
|
411
489
|
};
|
|
412
|
-
// Emit
|
|
413
|
-
// visually identical to pre-
|
|
490
|
+
// Emit optional modifiers only when set — keeps default-shape
|
|
491
|
+
// manifests visually identical to pre-modifier versions in
|
|
492
|
+
// JSON diffs.
|
|
414
493
|
if (fb._def.crdt !== undefined) {
|
|
415
494
|
f.crdt = fb._def.crdt;
|
|
416
495
|
}
|
|
496
|
+
if (fb._def.serverOnly) {
|
|
497
|
+
f.serverOnly = true;
|
|
498
|
+
}
|
|
499
|
+
if (fb._def.readonly) {
|
|
500
|
+
f.readonly = true;
|
|
501
|
+
}
|
|
417
502
|
return f;
|
|
418
503
|
}),
|
|
419
504
|
indexes: (e.indexes ?? []).map((idx) => ({
|