mirror of
https://github.com/HeyPuter/puter.git
synced 2026-07-08 08:12:15 +00:00
feat: pass args to all events (#3248)
* feat: pass args to all events * fix: alias app joining * fix: actor in event
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user