@pylonsync/sdk 0.3.258 → 0.3.261
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 +89 -5
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -164,6 +164,36 @@ interface FieldBuilder {
|
|
|
164
164
|
* sitting in SQLite.
|
|
165
165
|
*/
|
|
166
166
|
encrypted(): FieldBuilder;
|
|
167
|
+
/**
|
|
168
|
+
* Mark the field as the row's **owner** — the framework stamps it
|
|
169
|
+
* with `auth.userId` on every insert and refuses any client attempt
|
|
170
|
+
* to set it to a different user. This is what makes *optimistic,
|
|
171
|
+
* local-first writes the default* for owned data: the client can
|
|
172
|
+
* `db.insert("Offer", { buyerId, … })` directly (paints the row in
|
|
173
|
+
* the local store instantly, no server round-trip) and still be
|
|
174
|
+
* secure — the server overwrites/validates the owner from the
|
|
175
|
+
* session, so a forged `buyerId` is rejected before it ever lands.
|
|
176
|
+
*
|
|
177
|
+
* Concretely `.owner()`:
|
|
178
|
+
* - on **insert**, fills the field from `auth.userId` when omitted,
|
|
179
|
+
* and rejects a non-admin caller who supplies a *different*
|
|
180
|
+
* non-empty value (`OWNER_MISMATCH`, 403) — closing the IDOR
|
|
181
|
+
* shape where a policy gates on `data.ownerId == auth.userId`
|
|
182
|
+
* but the attacker just sends someone else's id;
|
|
183
|
+
* - on **update**, behaves like [`readonly`] — the owner can't be
|
|
184
|
+
* reassigned through the HTTP entity routes.
|
|
185
|
+
*
|
|
186
|
+
* Reach for this instead of writing a server function whenever the
|
|
187
|
+
* only server-authoritative part of a create is "who made it":
|
|
188
|
+
* `authorId`, `buyerId`, `sellerId`, `createdBy`, `userId`. Guests
|
|
189
|
+
* count — their stable guest id is stamped. Admin contexts may set
|
|
190
|
+
* an explicit value (migrations, tooling).
|
|
191
|
+
*
|
|
192
|
+
* Example: `sellerId: field.string().owner()` lets a listing be
|
|
193
|
+
* created with a plain optimistic `db.insert` while the seller id
|
|
194
|
+
* stays unspoofable.
|
|
195
|
+
*/
|
|
196
|
+
owner(): FieldBuilder;
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
function createFieldBuilder(type: FieldType): FieldBuilder {
|
|
@@ -191,6 +221,15 @@ function buildField(def: FieldDefinition): FieldBuilder {
|
|
|
191
221
|
encrypted() {
|
|
192
222
|
return buildField({ ...def, encrypted: true });
|
|
193
223
|
},
|
|
224
|
+
owner() {
|
|
225
|
+
// Dynamic default filled by the auth-aware pipeline; also lock
|
|
226
|
+
// the field on update so ownership can't be reassigned via PATCH.
|
|
227
|
+
return buildField({
|
|
228
|
+
...def,
|
|
229
|
+
readonly: true,
|
|
230
|
+
default: { kind: "owner" },
|
|
231
|
+
} as FieldDefinition);
|
|
232
|
+
},
|
|
194
233
|
};
|
|
195
234
|
}
|
|
196
235
|
|
|
@@ -440,6 +479,24 @@ export function definePlugin(def: PluginDefinition): PluginDefinition {
|
|
|
440
479
|
// Manifest generation
|
|
441
480
|
// ---------------------------------------------------------------------------
|
|
442
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Serialized form of `field.X().owner()`. Carried in a field's
|
|
484
|
+
* `default` slot as a *dynamic* default — the value isn't known until
|
|
485
|
+
* a request arrives, so the framework's auth-aware mutation pipeline
|
|
486
|
+
* (OwnerStampPlugin, Rust side) fills it from `auth.userId` on insert
|
|
487
|
+
* and rejects any client attempt to set it to a different user. The
|
|
488
|
+
* storage-layer default-filler (`apply_field_defaults`) deliberately
|
|
489
|
+
* skips this shape — it has no auth context, so it can't (and must
|
|
490
|
+
* not) stamp an owner.
|
|
491
|
+
*
|
|
492
|
+
* `$auth: "userId"` is the only kind today; the object shape leaves
|
|
493
|
+
* room for future auth-derived defaults (e.g. `{$auth:"tenantId"}`)
|
|
494
|
+
* without another manifest migration.
|
|
495
|
+
*/
|
|
496
|
+
export interface OwnerDefaultSentinel {
|
|
497
|
+
$auth: "userId";
|
|
498
|
+
}
|
|
499
|
+
|
|
443
500
|
export interface ManifestField {
|
|
444
501
|
name: string;
|
|
445
502
|
type: FieldType;
|
|
@@ -457,9 +514,14 @@ export interface ManifestField {
|
|
|
457
514
|
readonly?: boolean;
|
|
458
515
|
/** Default value to fill on insert when the row omits this field.
|
|
459
516
|
* - `"now"` → runtime stamps the current UTC time
|
|
517
|
+
* - `{$auth:…}` → a *dynamic* default the auth-aware mutation
|
|
518
|
+
* pipeline fills (see [`OwnerDefaultSentinel`]).
|
|
519
|
+
* Never stamped at the storage layer — the
|
|
520
|
+
* OwnerStampPlugin fills + enforces it.
|
|
460
521
|
* - any literal → runtime stamps that exact value
|
|
461
|
-
* Maps to `field.X().defaultNow()` / `.default(value)
|
|
462
|
-
|
|
522
|
+
* Maps to `field.X().defaultNow()` / `.default(value)` /
|
|
523
|
+
* `field.X().owner()`. */
|
|
524
|
+
default?: "now" | string | number | boolean | null | OwnerDefaultSentinel;
|
|
463
525
|
/** Allowed values for `field.enum([...])` — recorded so codegen
|
|
464
526
|
* can emit a literal-union type and runtime validation can
|
|
465
527
|
* reject out-of-set inserts. Plain `field.string()` doesn't
|
|
@@ -595,14 +657,22 @@ export function entitiesToManifest(
|
|
|
595
657
|
// using the procedural `field` exports get the same
|
|
596
658
|
// ManifestField shape as fluent apps.
|
|
597
659
|
const extra = fb._def as FieldDefinition & {
|
|
598
|
-
default?:
|
|
660
|
+
default?:
|
|
661
|
+
| { kind: "value"; value: unknown }
|
|
662
|
+
| { kind: "now" }
|
|
663
|
+
| { kind: "owner" };
|
|
599
664
|
enumValues?: readonly string[];
|
|
600
665
|
};
|
|
601
666
|
if (extra.default) {
|
|
602
667
|
f.default =
|
|
603
668
|
extra.default.kind === "now"
|
|
604
669
|
? "now"
|
|
605
|
-
:
|
|
670
|
+
: extra.default.kind === "owner"
|
|
671
|
+
? // Dynamic, auth-derived default. Carried as a sentinel
|
|
672
|
+
// object the Rust mutation pipeline recognizes; the
|
|
673
|
+
// storage-layer default-filler skips it (no auth there).
|
|
674
|
+
({ $auth: "userId" } satisfies OwnerDefaultSentinel)
|
|
675
|
+
: (extra.default.value as ManifestField["default"]);
|
|
606
676
|
}
|
|
607
677
|
if (extra.enumValues && extra.enumValues.length > 0) {
|
|
608
678
|
f.enumValues = extra.enumValues;
|
|
@@ -1416,7 +1486,10 @@ export const audit: Behavior = {
|
|
|
1416
1486
|
* below; this is just the shape stored on the field definition for
|
|
1417
1487
|
* the runtime to read.
|
|
1418
1488
|
*/
|
|
1419
|
-
type DefaultMarker =
|
|
1489
|
+
type DefaultMarker =
|
|
1490
|
+
| { kind: "value"; value: unknown }
|
|
1491
|
+
| { kind: "now" }
|
|
1492
|
+
| { kind: "owner" };
|
|
1420
1493
|
|
|
1421
1494
|
/**
|
|
1422
1495
|
* Augment FieldBuilder with the new `default()` / `defaultNow()`
|
|
@@ -1448,6 +1521,7 @@ function buildFieldWithDefaults(
|
|
|
1448
1521
|
): FieldBuilder & {
|
|
1449
1522
|
default(value: unknown): ReturnType<typeof buildFieldWithDefaults>;
|
|
1450
1523
|
defaultNow(): ReturnType<typeof buildFieldWithDefaults>;
|
|
1524
|
+
owner(): ReturnType<typeof buildFieldWithDefaults>;
|
|
1451
1525
|
} {
|
|
1452
1526
|
return {
|
|
1453
1527
|
_def: def,
|
|
@@ -1478,6 +1552,16 @@ function buildFieldWithDefaults(
|
|
|
1478
1552
|
defaultNow() {
|
|
1479
1553
|
return buildFieldWithDefaults({ ...def, default: { kind: "now" } });
|
|
1480
1554
|
},
|
|
1555
|
+
owner() {
|
|
1556
|
+
// Server-stamped owner: a dynamic default the auth-aware
|
|
1557
|
+
// pipeline fills + enforces, plus `readonly` so the owner can't
|
|
1558
|
+
// be reassigned via a later PATCH. See FieldBuilder.owner().
|
|
1559
|
+
return buildFieldWithDefaults({
|
|
1560
|
+
...def,
|
|
1561
|
+
readonly: true,
|
|
1562
|
+
default: { kind: "owner" },
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1481
1565
|
};
|
|
1482
1566
|
}
|
|
1483
1567
|
// Re-export `field` with the patched builder so callers picking up
|