feat: pass args to all events (#3248)
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled

* feat: pass args to all events

* fix: alias app joining

* fix: actor in event
This commit is contained in:
Daniel Salazar
2026-06-10 14:13:04 -07:00
committed by GitHub
parent b4837d49b6
commit 40d1c998bb
7 changed files with 189 additions and 16 deletions
+10 -4
View File
@@ -224,6 +224,8 @@ export type EventMap = {
entry: unknown;
host: string;
requestPath: string;
requestUrl?: string;
requestHash?: string;
mime: string;
};
@@ -298,7 +300,7 @@ export type LifecyclePhase = 'before' | 'after' | 'error' | 'reject';
/**
* Payload for `driver.<iface>.<method>.<phase>` events.
*
* One shape across all three phases; read `phase` (or the key suffix) to
* One shape across all phases; read `phase` (or the key suffix) to
* branch. `allow`/`rejectReason` are only meaningful on the `before` phase
* (emitted via `emitAndWait`).
*/
@@ -308,9 +310,11 @@ export type DriverMethodLifecycleEvent = {
method: string;
/** Resolved concrete driver name. */
driver: string;
/** Full actor object, if the request is authenticated. */
actor?: Actor;
/** Stable actor id (see `actorUid`), if the request is authenticated. */
actor?: string;
/** Call arguments. Present on `before`. */
actorUid?: string;
/** Call arguments. Present on every phase. */
args?: unknown;
/** Return value. Present on `after`. */
result?: unknown;
@@ -342,8 +346,10 @@ export type RouteLifecycleEvent = {
*/
req: ExpressRequest;
res: ExpressResponse;
/** Full actor object, if the request is authenticated. */
actor?: Actor;
/** Stable actor id (see `actorUid`), if the request is authenticated. */
actor?: string;
actorUid?: string;
/** Response status code. Present on `after`/`error`. */
statusCode?: number;
/** Wall-clock duration from `before` to terminal phase. */
@@ -491,21 +491,22 @@ describe('DriverController driver-method lifecycle events', () => {
server.clients.event.on(
'driver.puter-kvstore.set.before',
(_k, data) => {
if (data.actor === who) before.push(data);
if (data.actorUid === who) before.push(data);
},
);
server.clients.event.on(
'driver.puter-kvstore.set.after',
(_k, data) => {
if (data.actor === who) after.push(data);
if (data.actorUid === who) after.push(data);
},
);
const key = `lc-${uuidv4()}`;
const req = makeReq(
{
interface: 'puter-kvstore',
method: 'set',
args: { key: `lc-${uuidv4()}`, value: 'v' },
args: { key, value: 'v' },
},
actor,
);
@@ -522,9 +523,12 @@ describe('DriverController driver-method lifecycle events', () => {
phase: 'before',
iface: 'puter-kvstore',
method: 'set',
actor,
actorUid: who,
});
expect(after).toHaveLength(1);
expect(after[0].phase).toBe('after');
expect(after[0].args).toMatchObject({ key, value: 'v' });
expect(typeof after[0].durationMs).toBe('number');
});
@@ -574,6 +578,7 @@ describe('DriverController driver-method lifecycle events', () => {
expect(reject[0]).toMatchObject({
phase: 'reject',
method: 'set',
args: { key, value: 'v' },
rejectReason: 'blocked in test',
});
});
@@ -347,7 +347,8 @@ export class DriverController extends PuterController {
iface: ifaceName,
method,
driver: resolved,
actor,
actor: req.actor,
actorUid: actor,
args,
allow: true as boolean,
rejectReason: undefined as string | undefined,
@@ -365,7 +366,9 @@ export class DriverController extends PuterController {
iface: ifaceName,
method,
driver: resolved,
actor,
actor: req.actor,
actorUid: actor,
args,
rejectReason: beforeEvent.rejectReason,
},
{},
@@ -392,7 +395,9 @@ export class DriverController extends PuterController {
iface: ifaceName,
method,
driver: resolved,
actor,
actor: req.actor,
actorUid: actor,
args,
error: e,
durationMs: Date.now() - startedAt,
},
@@ -407,7 +412,9 @@ export class DriverController extends PuterController {
iface: ifaceName,
method,
driver: resolved,
actor,
actor: req.actor,
actorUid: actor,
args,
result,
durationMs: Date.now() - startedAt,
},
+2 -1
View File
@@ -119,7 +119,8 @@ describe('createRouteLifecycleMiddleware', () => {
phase: 'before',
method: 'post',
path: PATH,
actor: 'user:u-1',
actor: { user: { uuid: 'u-1' } },
actorUid: 'user:u-1',
});
// The live req/res are exposed so listeners can read the body or
// respond themselves.
+8 -1
View File
@@ -74,7 +74,14 @@ export const createRouteLifecycleMiddleware = (
return async (req: Request, res, next) => {
const actor = req.actor ? actorUid(req.actor) : undefined;
const startedAt = Date.now();
const base = { method, path: pathLabel, req, res, actor };
const base = {
method,
path: pathLabel,
req,
res,
actor: req.actor,
actorUid: actor,
};
const beforeEvent = {
phase: 'before' as const,
+17 -3
View File
@@ -912,7 +912,9 @@ export class AppDriver extends PuterDriver {
// `#ensure_puter_site_subdomain_is_owned`, `#ensureIndexUrlNotAlreadyInUse`,
// and the `app:canonicalUidAlias:*` kvstore pair). When a user creates
// or repoints an app at a puter-hosted subdomain (`*.puter.site`,
// `*.puter.app`, …) we want exactly one app row to back that URL:
// `*.puter.app`, …) — or at a custom host claimed by an
// `app_origin_aliases` group — we want exactly one app row to back
// that URL:
// • If a no-owner row exists (origin-bootstrap stub auto-created
// when an unknown origin first hit Puter) → claim it for the
// user, merge the new fields into it.
@@ -1219,7 +1221,8 @@ export class AppDriver extends PuterDriver {
/**
* Merge an incoming create/update into an existing app row that
* already owns the same puter-hosted index_url. Returns the joined
* already owns the same puter-hosted or alias-group index_url.
* Returns the joined
* (client-shaped) app on success, or `null` when no merge applied.
* Throws `app_index_url_already_in_use` when a conflict exists but
* cannot be merged (different owner, or same-owner non-bootstrap).
@@ -1236,7 +1239,18 @@ export class AppDriver extends PuterDriver {
excludeAppId,
} = {}) {
const indexUrl = object?.index_url;
if (!this.#isPuterHostedIndexUrl(indexUrl)) return null;
// Alias-group hosts (`app_origin_aliases`) get the same merge
// treatment as puter-hosted subdomains. Without this, a bootstrap
// stub on a custom domain could never be absorbed — and since
// `#findIndexUrlConflictRow` *does* honor alias groups, the
// uniqueness check below would hard-reject the owner's own
// create/update instead of merging into the stub.
if (
!this.#isPuterHostedIndexUrl(indexUrl) &&
!this.#findOriginAliasGroupForIndexUrl(indexUrl)
) {
return null;
}
const conflictRow = await this.#findIndexUrlConflictRow({
indexUrl,
+133
View File
@@ -948,3 +948,136 @@ describe('AppDriver.isNameAvailable additional branches', () => {
);
});
});
// ── alias-group custom domains (`app_origin_aliases`) ──────────────
//
// Custom hosts claimed by an alias group get the same bootstrap-stub
// merge treatment as puter-hosted subdomains: creating or repointing
// an app at an aliased host absorbs the unowned origin-bootstrap row
// instead of rejecting with `app_index_url_already_in_use`.
describe('AppDriver alias-group index_url merge', () => {
const aliasHostA = `alias-a-${Math.random().toString(36).slice(2, 10)}.test`;
const aliasHostB = `alias-b-${Math.random().toString(36).slice(2, 10)}.test`;
// `config` is protected on PuterDriver; reach in to toggle the alias
// groups for this block only. `#getOriginAliasGroups` reads config at
// call time, so runtime mutation takes effect immediately.
const driverConfig = () =>
(driver as unknown as { config: Record<string, unknown> }).config;
beforeAll(() => {
driverConfig().app_origin_aliases = [[aliasHostA], [aliasHostB]];
});
afterAll(() => {
delete driverConfig().app_origin_aliases;
});
const makeBootstrapStub = async (host: string) => {
const stubUid = `app-${uuidv4()}`;
// Mirrors AuthController's get-user-app-token bootstrap path:
// origin persisted as index_url, no owner, name === uid.
await server.stores.app.createFromOrigin(stubUid, `https://${host}`);
return stubUid;
};
it('create at an aliased host absorbs the unowned bootstrap stub', async () => {
const { actor, userId } = await makeUser();
const stubUid = await makeBootstrapStub(aliasHostA);
const name = uniqueName('alias-create');
const result = await withActor(actor, () =>
driver.create({
object: {
name,
title: 'Aliased',
index_url: `https://${aliasHostA}/`,
},
}),
);
// The stub row survives as the canonical app, claimed + merged.
expect(result.uid).toBe(stubUid);
expect(result.name).toBe(name);
const stored = await server.stores.app.getByUid(stubUid);
expect(stored?.owner_user_id).toBe(userId);
});
it('rejects another user registering an app under a reserved aliased host', async () => {
const other = await makeUser();
await expect(
withActor(other.actor, () =>
driver.create({
object: {
name: uniqueName('squatter'),
title: 't',
index_url: `https://${aliasHostA}/index.html`,
},
}),
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('update repointing to an aliased host merges and aliases the old uid', async () => {
const { actor, userId } = await makeUser();
const stubUid = await makeBootstrapStub(aliasHostB);
const created = await withActor(actor, () =>
driver.create({
object: {
name: uniqueName('alias-upd'),
title: 't',
index_url: uniqueIndexUrl(),
},
}),
);
const updated = await withActor(actor, () =>
driver.update({
uid: created.uid,
object: { index_url: `https://${aliasHostB}/` },
}),
);
// Merged into the stub; source row deleted; old uid still resolves
// via the canonical-uid alias.
expect(updated.uid).toBe(stubUid);
expect(await server.stores.app.getByUid(created.uid as string)).toBeNull();
const stored = await server.stores.app.getByUid(stubUid);
expect(stored?.owner_user_id).toBe(userId);
const viaOldUid = await withActor(actor, () =>
driver.read({ uid: created.uid }),
);
expect(viaOldUid.uid).toBe(stubUid);
});
it('leaves unrelated custom domains untouched (no alias group, no conflict check)', async () => {
const a = await makeUser();
const b = await makeUser();
const sharedUrl = uniqueIndexUrl();
// Non-puter, non-aliased hosts keep their historical behavior:
// no uniqueness enforcement, both creates succeed.
const first = await withActor(a.actor, () =>
driver.create({
object: {
name: uniqueName('plain-a'),
title: 't',
index_url: sharedUrl,
},
}),
);
const second = await withActor(b.actor, () =>
driver.create({
object: {
name: uniqueName('plain-b'),
title: 't',
index_url: sharedUrl,
},
}),
);
expect(first.uid).not.toBe(second.uid);
});
});