@pylonsync/sdk 0.3.157 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +87 -2
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.157",
6
+ "version": "0.3.161",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
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 `crdt` only when set — keeps default-shape manifests
413
- // visually identical to pre-CRDT versions in JSON diffs.
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) => ({