@spfn/auth 0.3.0-beta.7 → 0.3.0-beta.9
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/README.md +409 -7
- package/dist/client-proof.d.ts +23 -12
- package/dist/client-proof.js +143 -4
- package/dist/client-proof.js.map +1 -1
- package/dist/config.d.ts +38 -0
- package/dist/config.js +20 -4
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +97 -3
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +46 -6
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -1
- package/dist/{authenticate-Mg9D7Nys.d.ts → machine-principals-cC9TQZIt.d.ts} +347 -6
- package/dist/nextjs/api.js +190 -9
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js +59 -8
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +736 -89
- package/dist/server.js +1511 -454
- package/dist/server.js.map +1 -1
- package/migrations/20260901091716_fine_arclight/migration.sql +21 -0
- package/migrations/20260901091716_fine_arclight/snapshot.json +3849 -0
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -4473,7 +4473,7 @@ var init_types = __esm({
|
|
|
4473
4473
|
|
|
4474
4474
|
// src/server/routes/schema.ts
|
|
4475
4475
|
import { EMAIL_PATTERN, PHONE_PATTERN } from "@spfn/auth";
|
|
4476
|
-
var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
|
|
4476
|
+
var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PublicKeySchema, KeyIdSchema, FingerprintSchema, UserCodeSchema, DeviceAuthPollResponseSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
|
|
4477
4477
|
var init_schema3 = __esm({
|
|
4478
4478
|
"src/server/routes/schema.ts"() {
|
|
4479
4479
|
"use strict";
|
|
@@ -4495,6 +4495,39 @@ var init_schema3 = __esm({
|
|
|
4495
4495
|
KEY_PLATFORM.map((platform) => Type.Literal(platform)),
|
|
4496
4496
|
{ description: "Platform the key lives on" }
|
|
4497
4497
|
);
|
|
4498
|
+
PublicKeySchema = Type.String({
|
|
4499
|
+
maxLength: 2048,
|
|
4500
|
+
description: "Client public key, SPKI DER in base64"
|
|
4501
|
+
});
|
|
4502
|
+
KeyIdSchema = Type.String({
|
|
4503
|
+
maxLength: 64,
|
|
4504
|
+
description: "Key identifier"
|
|
4505
|
+
});
|
|
4506
|
+
FingerprintSchema = Type.String({
|
|
4507
|
+
maxLength: 128,
|
|
4508
|
+
description: "SHA-256 hex fingerprint of the public key"
|
|
4509
|
+
});
|
|
4510
|
+
UserCodeSchema = Type.String({
|
|
4511
|
+
minLength: 8,
|
|
4512
|
+
maxLength: 16,
|
|
4513
|
+
description: "Device user code as displayed, e.g. WXYZ-2345. Dashes, spaces and case are ignored."
|
|
4514
|
+
});
|
|
4515
|
+
DeviceAuthPollResponseSchema = Type.Union([
|
|
4516
|
+
Type.Object({
|
|
4517
|
+
status: Type.Literal("pending"),
|
|
4518
|
+
intervalMillis: Type.Integer({ description: "Milliseconds to wait before polling again" })
|
|
4519
|
+
}),
|
|
4520
|
+
Type.Object({
|
|
4521
|
+
status: Type.Literal("approved"),
|
|
4522
|
+
userId: Type.String(),
|
|
4523
|
+
publicId: Type.String(),
|
|
4524
|
+
email: Type.Optional(Type.String()),
|
|
4525
|
+
phone: Type.Optional(Type.String()),
|
|
4526
|
+
passwordChangeRequired: Type.Boolean()
|
|
4527
|
+
})
|
|
4528
|
+
], {
|
|
4529
|
+
description: "Pending, or the login the approval produced"
|
|
4530
|
+
});
|
|
4498
4531
|
PasswordSchema = Type.String({
|
|
4499
4532
|
minLength: 8,
|
|
4500
4533
|
maxLength: 72,
|
|
@@ -5028,9 +5061,72 @@ var init_signup_link_tokens = __esm({
|
|
|
5028
5061
|
}
|
|
5029
5062
|
});
|
|
5030
5063
|
|
|
5064
|
+
// src/server/entities/device-authorizations.ts
|
|
5065
|
+
import { text as text8, uniqueIndex as uniqueIndex3 } from "drizzle-orm/pg-core";
|
|
5066
|
+
import { id as id8, timestamps as timestamps7, enumText as enumText5, utcTimestamp as utcTimestamp6, optionalForeignKey } from "@spfn/core/db";
|
|
5067
|
+
var DEVICE_AUTH_STATUSES, deviceAuthorizations;
|
|
5068
|
+
var init_device_authorizations = __esm({
|
|
5069
|
+
"src/server/entities/device-authorizations.ts"() {
|
|
5070
|
+
"use strict";
|
|
5071
|
+
init_types();
|
|
5072
|
+
init_users();
|
|
5073
|
+
init_schema4();
|
|
5074
|
+
DEVICE_AUTH_STATUSES = ["pending", "approved", "denied", "consumed"];
|
|
5075
|
+
deviceAuthorizations = authSchema.table(
|
|
5076
|
+
"device_authorizations",
|
|
5077
|
+
{
|
|
5078
|
+
id: id8(),
|
|
5079
|
+
// SHA-256 of the device code, hex
|
|
5080
|
+
// The code itself (32 random bytes, base64url) is returned to the waiting
|
|
5081
|
+
// device once and never stored: it is the only thing that device holds,
|
|
5082
|
+
// so a dump of this table must not let its reader finish someone's login
|
|
5083
|
+
deviceCodeHash: text8("device_code_hash").notNull().unique(),
|
|
5084
|
+
// The code a person reads off the waiting device's screen
|
|
5085
|
+
// Stored normalized — uppercase, no dash — because that is the only form
|
|
5086
|
+
// a lookup can match; the dash is put back for display
|
|
5087
|
+
// Plaintext on purpose: it authorizes nothing without an approver who is
|
|
5088
|
+
// already authenticated, and it has to be found by exact match
|
|
5089
|
+
userCode: text8("user_code").notNull(),
|
|
5090
|
+
// Key material the waiting device generated, same shapes as user_public_keys
|
|
5091
|
+
// Parked, not registered: it can sign nothing until a poll moves it over
|
|
5092
|
+
publicKey: text8("public_key").notNull(),
|
|
5093
|
+
keyId: text8("key_id").notNull(),
|
|
5094
|
+
fingerprint: text8("fingerprint").notNull(),
|
|
5095
|
+
algorithm: enumText5("algorithm", KEY_ALGORITHM).notNull().default("ES256"),
|
|
5096
|
+
// Device labels the waiting device supplied
|
|
5097
|
+
// null: it sent none
|
|
5098
|
+
// Shown to the approver so they can tell whether the device asking is the
|
|
5099
|
+
// one in front of them — display only, so a client that lies gains nothing
|
|
5100
|
+
// but a wrong line on the approval screen
|
|
5101
|
+
deviceName: text8("device_name"),
|
|
5102
|
+
platform: enumText5("platform", KEY_PLATFORM),
|
|
5103
|
+
status: enumText5("status", DEVICE_AUTH_STATUSES).notNull().default("pending"),
|
|
5104
|
+
// Who approved, written at approve time from the approver's session
|
|
5105
|
+
// null while pending, and on every record that was denied or expired
|
|
5106
|
+
// Never taken from a request body — that would be the whole authorization
|
|
5107
|
+
userId: optionalForeignKey("user", () => users.id, { onDelete: "cascade" }),
|
|
5108
|
+
// Expiry — 10 minutes from creation by default
|
|
5109
|
+
// Judged on read; no job clears the row
|
|
5110
|
+
expiresAt: utcTimestamp6("expires_at").notNull(),
|
|
5111
|
+
// Set when the approver said yes
|
|
5112
|
+
approvedAt: utcTimestamp6("approved_at"),
|
|
5113
|
+
// Set when the poll that registered the key won the race
|
|
5114
|
+
// Non-null means the record is spent, whatever else it says
|
|
5115
|
+
consumedAt: utcTimestamp6("consumed_at"),
|
|
5116
|
+
...timestamps7()
|
|
5117
|
+
},
|
|
5118
|
+
(table) => [
|
|
5119
|
+
// Lookup path for info/approve/deny
|
|
5120
|
+
// Unique so a typed code can never address two records
|
|
5121
|
+
uniqueIndex3("device_authorization_user_code_idx").on(table.userCode)
|
|
5122
|
+
]
|
|
5123
|
+
);
|
|
5124
|
+
}
|
|
5125
|
+
});
|
|
5126
|
+
|
|
5031
5127
|
// src/server/entities/user-invitations.ts
|
|
5032
|
-
import { text as
|
|
5033
|
-
import { id as
|
|
5128
|
+
import { text as text9, index as index8 } from "drizzle-orm/pg-core";
|
|
5129
|
+
import { id as id9, timestamps as timestamps8, enumText as enumText6, utcTimestamp as utcTimestamp7, typedJsonb as typedJsonb2, foreignKey as foreignKey5 } from "@spfn/core/db";
|
|
5034
5130
|
var userInvitations;
|
|
5035
5131
|
var init_user_invitations = __esm({
|
|
5036
5132
|
"src/server/entities/user-invitations.ts"() {
|
|
@@ -5043,14 +5139,14 @@ var init_user_invitations = __esm({
|
|
|
5043
5139
|
"user_invitations",
|
|
5044
5140
|
{
|
|
5045
5141
|
// Primary key
|
|
5046
|
-
id:
|
|
5142
|
+
id: id9(),
|
|
5047
5143
|
// Target email address for the invitation
|
|
5048
5144
|
// Will become the user's email upon acceptance
|
|
5049
|
-
email:
|
|
5145
|
+
email: text9("email").notNull(),
|
|
5050
5146
|
// Unique invitation token (UUID v4)
|
|
5051
5147
|
// Used in invitation URL: /auth/invite/{token}
|
|
5052
5148
|
// Single-use token that expires after acceptance
|
|
5053
|
-
token:
|
|
5149
|
+
token: text9("token").notNull().unique(),
|
|
5054
5150
|
// Role to be assigned when invitation is accepted
|
|
5055
5151
|
// Foreign key to roles table
|
|
5056
5152
|
roleId: foreignKey5("role", () => roles.id),
|
|
@@ -5063,19 +5159,19 @@ var init_user_invitations = __esm({
|
|
|
5063
5159
|
// - accepted: User accepted and account created
|
|
5064
5160
|
// - expired: Invitation expired (automatic)
|
|
5065
5161
|
// - cancelled: Invitation cancelled by admin
|
|
5066
|
-
status:
|
|
5162
|
+
status: enumText6("status", INVITATION_STATUSES).default("pending").notNull(),
|
|
5067
5163
|
// Expiration timestamp (default: 7 days from creation)
|
|
5068
5164
|
// Invitation cannot be accepted after this time
|
|
5069
5165
|
// Background job should update status to 'expired'
|
|
5070
|
-
expiresAt:
|
|
5166
|
+
expiresAt: utcTimestamp7("expires_at").notNull(),
|
|
5071
5167
|
// Timestamp when invitation was accepted
|
|
5072
5168
|
// null = not yet accepted
|
|
5073
5169
|
// Used for: audit trail, analytics
|
|
5074
|
-
acceptedAt:
|
|
5170
|
+
acceptedAt: utcTimestamp7("accepted_at"),
|
|
5075
5171
|
// Timestamp when invitation was cancelled
|
|
5076
5172
|
// null = not cancelled
|
|
5077
5173
|
// Used for: audit trail
|
|
5078
|
-
cancelledAt:
|
|
5174
|
+
cancelledAt: utcTimestamp7("cancelled_at"),
|
|
5079
5175
|
// Additional metadata (JSONB)
|
|
5080
5176
|
// Use cases:
|
|
5081
5177
|
// - Custom welcome message
|
|
@@ -5084,7 +5180,7 @@ var init_user_invitations = __esm({
|
|
|
5084
5180
|
// - Custom fields for app-specific data
|
|
5085
5181
|
// Example: { message: "Welcome!", department: "Engineering" }
|
|
5086
5182
|
metadata: typedJsonb2("metadata"),
|
|
5087
|
-
...
|
|
5183
|
+
...timestamps8()
|
|
5088
5184
|
},
|
|
5089
5185
|
(table) => [
|
|
5090
5186
|
// Indexes for query optimization
|
|
@@ -5101,9 +5197,9 @@ var init_user_invitations = __esm({
|
|
|
5101
5197
|
});
|
|
5102
5198
|
|
|
5103
5199
|
// src/server/entities/account-deletion-requests.ts
|
|
5104
|
-
import { text as
|
|
5200
|
+
import { text as text10, index as index9, uniqueIndex as uniqueIndex4 } from "drizzle-orm/pg-core";
|
|
5105
5201
|
import { sql as sql2 } from "drizzle-orm";
|
|
5106
|
-
import { id as
|
|
5202
|
+
import { id as id10, timestamps as timestamps9, enumText as enumText7, utcTimestamp as utcTimestamp8, optionalForeignKey as optionalForeignKey2 } from "@spfn/core/db";
|
|
5107
5203
|
var accountDeletionRequests;
|
|
5108
5204
|
var init_account_deletion_requests = __esm({
|
|
5109
5205
|
"src/server/entities/account-deletion-requests.ts"() {
|
|
@@ -5114,32 +5210,32 @@ var init_account_deletion_requests = __esm({
|
|
|
5114
5210
|
accountDeletionRequests = authSchema.table(
|
|
5115
5211
|
"account_deletion_requests",
|
|
5116
5212
|
{
|
|
5117
|
-
id:
|
|
5213
|
+
id: id10(),
|
|
5118
5214
|
// Foreign key to users table. `set null` (optionalForeignKey default) so this
|
|
5119
5215
|
// row survives a hard-delete purge of the user it refers to.
|
|
5120
|
-
userId:
|
|
5216
|
+
userId: optionalForeignKey2("user", () => users.id),
|
|
5121
5217
|
// Snapshot of the user's public UUID at request time — stays readable even
|
|
5122
5218
|
// after userId is nulled out or the account is anonymized.
|
|
5123
|
-
userPublicId:
|
|
5219
|
+
userPublicId: text10("user_public_id").notNull(),
|
|
5124
5220
|
// When the deletion was requested
|
|
5125
|
-
requestedAt:
|
|
5221
|
+
requestedAt: utcTimestamp8("requested_at").notNull().defaultNow(),
|
|
5126
5222
|
// When the purge job is allowed to run (requestedAt + grace period; equals
|
|
5127
5223
|
// requestedAt itself for immediate/zero-grace deletions)
|
|
5128
|
-
purgeScheduledAt:
|
|
5224
|
+
purgeScheduledAt: utcTimestamp8("purge_scheduled_at").notNull(),
|
|
5129
5225
|
// Request lifecycle status
|
|
5130
5226
|
// - pending: awaiting purgeScheduledAt (or immediate purge)
|
|
5131
5227
|
// - cancelled: recovered before purge
|
|
5132
5228
|
// - completed: purge ran
|
|
5133
|
-
status:
|
|
5229
|
+
status: enumText7("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
|
|
5134
5230
|
// Who initiated the request
|
|
5135
|
-
requestedBy:
|
|
5231
|
+
requestedBy: enumText7("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
|
|
5136
5232
|
// Optional free-text reason (self-service UI, admin note, DSR reference, ...)
|
|
5137
|
-
reason:
|
|
5138
|
-
cancelledAt:
|
|
5139
|
-
completedAt:
|
|
5233
|
+
reason: text10("reason"),
|
|
5234
|
+
cancelledAt: utcTimestamp8("cancelled_at"),
|
|
5235
|
+
completedAt: utcTimestamp8("completed_at"),
|
|
5140
5236
|
// Purge strategy actually executed (set on completion; null while pending)
|
|
5141
|
-
purgeStrategy:
|
|
5142
|
-
...
|
|
5237
|
+
purgeStrategy: enumText7("purge_strategy", PURGE_STRATEGIES),
|
|
5238
|
+
...timestamps9()
|
|
5143
5239
|
},
|
|
5144
5240
|
(table) => [
|
|
5145
5241
|
index9("account_deletion_requests_user_id_idx").on(table.userId),
|
|
@@ -5147,7 +5243,7 @@ var init_account_deletion_requests = __esm({
|
|
|
5147
5243
|
index9("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
|
|
5148
5244
|
index9("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
|
|
5149
5245
|
// Partial unique index: at most one pending request per user at a time.
|
|
5150
|
-
|
|
5246
|
+
uniqueIndex4("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
|
|
5151
5247
|
]
|
|
5152
5248
|
);
|
|
5153
5249
|
}
|
|
@@ -5299,8 +5395,8 @@ var init_rbac = __esm({
|
|
|
5299
5395
|
});
|
|
5300
5396
|
|
|
5301
5397
|
// src/server/entities/permissions.ts
|
|
5302
|
-
import { text as
|
|
5303
|
-
import { id as
|
|
5398
|
+
import { text as text11, boolean as boolean4, index as index10 } from "drizzle-orm/pg-core";
|
|
5399
|
+
import { id as id11, timestamps as timestamps10, enumText as enumText8, typedJsonb as typedJsonb3 } from "@spfn/core/db";
|
|
5304
5400
|
var permissions;
|
|
5305
5401
|
var init_permissions = __esm({
|
|
5306
5402
|
"src/server/entities/permissions.ts"() {
|
|
@@ -5311,7 +5407,7 @@ var init_permissions = __esm({
|
|
|
5311
5407
|
"permissions",
|
|
5312
5408
|
{
|
|
5313
5409
|
// Primary key
|
|
5314
|
-
id:
|
|
5410
|
+
id: id11(),
|
|
5315
5411
|
// Permission identifier
|
|
5316
5412
|
// Format: resource:action or namespace:resource:action
|
|
5317
5413
|
// Examples:
|
|
@@ -5319,20 +5415,20 @@ var init_permissions = __esm({
|
|
|
5319
5415
|
// - Namespaced: 'auth:user:delete', 'cms:post:publish'
|
|
5320
5416
|
// Must be unique across all permissions
|
|
5321
5417
|
// Used in: permission checks, role assignments, API guards
|
|
5322
|
-
name:
|
|
5418
|
+
name: text11("name").notNull().unique(),
|
|
5323
5419
|
// Display name for UI
|
|
5324
5420
|
// Human-readable name shown in admin panels
|
|
5325
5421
|
// Example: "Delete Users", "Publish Posts"
|
|
5326
|
-
displayName:
|
|
5422
|
+
displayName: text11("display_name").notNull(),
|
|
5327
5423
|
// Permission description
|
|
5328
5424
|
// Detailed explanation of what this permission allows
|
|
5329
5425
|
// Example: "Allows deletion of user accounts from the system"
|
|
5330
|
-
description:
|
|
5426
|
+
description: text11("description"),
|
|
5331
5427
|
// Category for grouping
|
|
5332
5428
|
// Used for: organizing permissions in UI, filtering
|
|
5333
5429
|
// Built-in categories: auth, user, rbac, system
|
|
5334
5430
|
// Custom categories: any app-specific category
|
|
5335
|
-
category:
|
|
5431
|
+
category: enumText8("category", PERMISSION_CATEGORIES),
|
|
5336
5432
|
// Built-in permission flag
|
|
5337
5433
|
// true: Core package permissions (auth:*, user:*, rbac:*)
|
|
5338
5434
|
// - Cannot be deleted or modified
|
|
@@ -5363,7 +5459,7 @@ var init_permissions = __esm({
|
|
|
5363
5459
|
// - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
|
|
5364
5460
|
// Example: { icon: 'trash', color: 'red', requiresMfa: true }
|
|
5365
5461
|
metadata: typedJsonb3("metadata"),
|
|
5366
|
-
...
|
|
5462
|
+
...timestamps10()
|
|
5367
5463
|
},
|
|
5368
5464
|
(table) => [
|
|
5369
5465
|
index10("permissions_name_idx").on(table.name),
|
|
@@ -5378,7 +5474,7 @@ var init_permissions = __esm({
|
|
|
5378
5474
|
|
|
5379
5475
|
// src/server/entities/role-permissions.ts
|
|
5380
5476
|
import { index as index11, unique } from "drizzle-orm/pg-core";
|
|
5381
|
-
import { id as
|
|
5477
|
+
import { id as id12, timestamps as timestamps11, foreignKey as foreignKey6 } from "@spfn/core/db";
|
|
5382
5478
|
var rolePermissions;
|
|
5383
5479
|
var init_role_permissions = __esm({
|
|
5384
5480
|
"src/server/entities/role-permissions.ts"() {
|
|
@@ -5390,7 +5486,7 @@ var init_role_permissions = __esm({
|
|
|
5390
5486
|
"role_permissions",
|
|
5391
5487
|
{
|
|
5392
5488
|
// Primary key
|
|
5393
|
-
id:
|
|
5489
|
+
id: id12(),
|
|
5394
5490
|
// Role reference
|
|
5395
5491
|
// Foreign key to roles table
|
|
5396
5492
|
// Cascade delete: when role is deleted, all role-permission mappings are removed
|
|
@@ -5403,7 +5499,7 @@ var init_role_permissions = __esm({
|
|
|
5403
5499
|
// Used for: granting permissions to roles
|
|
5404
5500
|
// Example: user:delete permission → [Admin, Superadmin]
|
|
5405
5501
|
permissionId: foreignKey6("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
5406
|
-
...
|
|
5502
|
+
...timestamps11()
|
|
5407
5503
|
},
|
|
5408
5504
|
(table) => [
|
|
5409
5505
|
// Indexes for query performance
|
|
@@ -5417,8 +5513,8 @@ var init_role_permissions = __esm({
|
|
|
5417
5513
|
});
|
|
5418
5514
|
|
|
5419
5515
|
// src/server/entities/user-permissions.ts
|
|
5420
|
-
import { boolean as boolean5, text as
|
|
5421
|
-
import { id as
|
|
5516
|
+
import { boolean as boolean5, text as text12, index as index12, unique as unique2 } from "drizzle-orm/pg-core";
|
|
5517
|
+
import { id as id13, timestamps as timestamps12, utcTimestamp as utcTimestamp9, foreignKey as foreignKey7 } from "@spfn/core/db";
|
|
5422
5518
|
var userPermissions;
|
|
5423
5519
|
var init_user_permissions = __esm({
|
|
5424
5520
|
"src/server/entities/user-permissions.ts"() {
|
|
@@ -5430,7 +5526,7 @@ var init_user_permissions = __esm({
|
|
|
5430
5526
|
"user_permissions",
|
|
5431
5527
|
{
|
|
5432
5528
|
// Primary key
|
|
5433
|
-
id:
|
|
5529
|
+
id: id13(),
|
|
5434
5530
|
// User reference
|
|
5435
5531
|
// Foreign key to users table
|
|
5436
5532
|
// Cascade delete: when user is deleted, all overrides are removed
|
|
@@ -5453,13 +5549,13 @@ var init_user_permissions = __esm({
|
|
|
5453
5549
|
// Reason for grant/revocation
|
|
5454
5550
|
// Used for: audit trail, compliance documentation
|
|
5455
5551
|
// Example: "Temporary access for project X", "Security incident - restricted"
|
|
5456
|
-
reason:
|
|
5552
|
+
reason: text12("reason"),
|
|
5457
5553
|
// Expiration timestamp (optional)
|
|
5458
5554
|
// null: Permanent override (remains until manually removed)
|
|
5459
5555
|
// timestamp: Permission expires at this time (auto-revoked by background job)
|
|
5460
5556
|
// Use case: Time-limited elevated access, temporary restrictions
|
|
5461
|
-
expiresAt:
|
|
5462
|
-
...
|
|
5557
|
+
expiresAt: utcTimestamp9("expires_at"),
|
|
5558
|
+
...timestamps12()
|
|
5463
5559
|
},
|
|
5464
5560
|
(table) => [
|
|
5465
5561
|
// Indexes for query performance
|
|
@@ -5475,7 +5571,7 @@ var init_user_permissions = __esm({
|
|
|
5475
5571
|
|
|
5476
5572
|
// src/server/entities/auth-metadata.ts
|
|
5477
5573
|
import { sql as sql3 } from "drizzle-orm";
|
|
5478
|
-
import { text as
|
|
5574
|
+
import { text as text13, timestamp } from "drizzle-orm/pg-core";
|
|
5479
5575
|
var authMetadata;
|
|
5480
5576
|
var init_auth_metadata = __esm({
|
|
5481
5577
|
"src/server/entities/auth-metadata.ts"() {
|
|
@@ -5485,9 +5581,9 @@ var init_auth_metadata = __esm({
|
|
|
5485
5581
|
"auth_metadata",
|
|
5486
5582
|
{
|
|
5487
5583
|
// Metadata key (primary key)
|
|
5488
|
-
key:
|
|
5584
|
+
key: text13("key").primaryKey(),
|
|
5489
5585
|
// Metadata value
|
|
5490
|
-
value:
|
|
5586
|
+
value: text13("value").notNull(),
|
|
5491
5587
|
// Last updated timestamp — stamped by the database on insert and on update
|
|
5492
5588
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
|
|
5493
5589
|
}
|
|
@@ -5496,8 +5592,8 @@ var init_auth_metadata = __esm({
|
|
|
5496
5592
|
});
|
|
5497
5593
|
|
|
5498
5594
|
// src/server/entities/ops-tokens.ts
|
|
5499
|
-
import { text as
|
|
5500
|
-
import { id as
|
|
5595
|
+
import { text as text14 } from "drizzle-orm/pg-core";
|
|
5596
|
+
import { id as id14, timestamps as timestamps13, utcTimestamp as utcTimestamp10 } from "@spfn/core/db";
|
|
5501
5597
|
var opsTokens;
|
|
5502
5598
|
var init_ops_tokens = __esm({
|
|
5503
5599
|
"src/server/entities/ops-tokens.ts"() {
|
|
@@ -5506,22 +5602,22 @@ var init_ops_tokens = __esm({
|
|
|
5506
5602
|
opsTokens = authSchema.table(
|
|
5507
5603
|
"ops_tokens",
|
|
5508
5604
|
{
|
|
5509
|
-
id:
|
|
5605
|
+
id: id14(),
|
|
5510
5606
|
// Operator-facing label ("ci-deploy", "rayim-laptop")
|
|
5511
|
-
name:
|
|
5607
|
+
name: text14("name").notNull(),
|
|
5512
5608
|
// SHA-256 hex of the token secret. Lookup key — the secret never lands
|
|
5513
5609
|
// here, and the unique constraint doubles as the lookup index.
|
|
5514
|
-
tokenHash:
|
|
5610
|
+
tokenHash: text14("token_hash").notNull().unique(),
|
|
5515
5611
|
// Granted scopes as permission strings ('waitlist:read', ...).
|
|
5516
5612
|
// '*' grants every scope.
|
|
5517
|
-
scopes:
|
|
5613
|
+
scopes: text14("scopes").array().notNull(),
|
|
5518
5614
|
// null = the token does not expire
|
|
5519
|
-
expiresAt:
|
|
5615
|
+
expiresAt: utcTimestamp10("expires_at"),
|
|
5520
5616
|
// null = active; a timestamp revokes the token permanently
|
|
5521
|
-
revokedAt:
|
|
5617
|
+
revokedAt: utcTimestamp10("revoked_at"),
|
|
5522
5618
|
// Last successful verification, updated fire-and-forget
|
|
5523
|
-
lastUsedAt:
|
|
5524
|
-
...
|
|
5619
|
+
lastUsedAt: utcTimestamp10("last_used_at"),
|
|
5620
|
+
...timestamps13()
|
|
5525
5621
|
}
|
|
5526
5622
|
);
|
|
5527
5623
|
}
|
|
@@ -5538,6 +5634,7 @@ var init_entities = __esm({
|
|
|
5538
5634
|
init_user_social_accounts();
|
|
5539
5635
|
init_verification_codes();
|
|
5540
5636
|
init_signup_link_tokens();
|
|
5637
|
+
init_device_authorizations();
|
|
5541
5638
|
init_user_invitations();
|
|
5542
5639
|
init_account_deletion_requests();
|
|
5543
5640
|
init_roles();
|
|
@@ -5564,8 +5661,8 @@ var init_users_repository = __esm({
|
|
|
5564
5661
|
* ID로 사용자 조회
|
|
5565
5662
|
* Read replica 사용
|
|
5566
5663
|
*/
|
|
5567
|
-
async findById(
|
|
5568
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
5664
|
+
async findById(id15) {
|
|
5665
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id15)).limit(1);
|
|
5569
5666
|
return result[0] ?? null;
|
|
5570
5667
|
}
|
|
5571
5668
|
/**
|
|
@@ -5575,8 +5672,8 @@ var init_users_repository = __esm({
|
|
|
5575
5672
|
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
5576
5673
|
* 계속 사용할 것.
|
|
5577
5674
|
*/
|
|
5578
|
-
async findByIdOnPrimary(
|
|
5579
|
-
const result = await this.db.select().from(users).where(eq(users.id,
|
|
5675
|
+
async findByIdOnPrimary(id15) {
|
|
5676
|
+
const result = await this.db.select().from(users).where(eq(users.id, id15)).limit(1);
|
|
5580
5677
|
return result[0] ?? null;
|
|
5581
5678
|
}
|
|
5582
5679
|
/**
|
|
@@ -5647,13 +5744,13 @@ var init_users_repository = __esm({
|
|
|
5647
5744
|
*
|
|
5648
5745
|
* roleId가 null인 유저는 role: null 반환
|
|
5649
5746
|
*/
|
|
5650
|
-
async findByIdWithRole(
|
|
5747
|
+
async findByIdWithRole(id15) {
|
|
5651
5748
|
const result = await this.readDb.select({
|
|
5652
5749
|
user: users,
|
|
5653
5750
|
roleName: roles.name,
|
|
5654
5751
|
roleDisplayName: roles.displayName,
|
|
5655
5752
|
rolePriority: roles.priority
|
|
5656
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
5753
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id15)).limit(1);
|
|
5657
5754
|
const row = result[0];
|
|
5658
5755
|
if (!row) {
|
|
5659
5756
|
return null;
|
|
@@ -5728,9 +5825,9 @@ var init_users_repository = __esm({
|
|
|
5728
5825
|
* 사용자 정보 업데이트
|
|
5729
5826
|
* Write primary 사용
|
|
5730
5827
|
*/
|
|
5731
|
-
async updateById(
|
|
5828
|
+
async updateById(id15, data) {
|
|
5732
5829
|
const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
|
|
5733
|
-
const result = await this.db.update(users).set(patch).where(eq(users.id,
|
|
5830
|
+
const result = await this.db.update(users).set(patch).where(eq(users.id, id15)).returning();
|
|
5734
5831
|
return result[0] ?? null;
|
|
5735
5832
|
}
|
|
5736
5833
|
/**
|
|
@@ -5742,10 +5839,10 @@ var init_users_repository = __esm({
|
|
|
5742
5839
|
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
5743
5840
|
* Write primary 사용
|
|
5744
5841
|
*/
|
|
5745
|
-
async reactivateFromPendingDeletion(
|
|
5842
|
+
async reactivateFromPendingDeletion(id15) {
|
|
5746
5843
|
const result = await this.db.update(users).set({ status: "active" }).where(
|
|
5747
5844
|
and(
|
|
5748
|
-
eq(users.id,
|
|
5845
|
+
eq(users.id, id15),
|
|
5749
5846
|
eq(users.status, "pending_deletion")
|
|
5750
5847
|
)
|
|
5751
5848
|
).returning();
|
|
@@ -5755,32 +5852,32 @@ var init_users_repository = __esm({
|
|
|
5755
5852
|
* 비밀번호 업데이트
|
|
5756
5853
|
* Write primary 사용
|
|
5757
5854
|
*/
|
|
5758
|
-
async updatePassword(
|
|
5855
|
+
async updatePassword(id15, passwordHash, clearPasswordChangeRequired = true) {
|
|
5759
5856
|
const updateData = {
|
|
5760
5857
|
passwordHash
|
|
5761
5858
|
};
|
|
5762
5859
|
if (clearPasswordChangeRequired) {
|
|
5763
5860
|
updateData.passwordChangeRequired = false;
|
|
5764
5861
|
}
|
|
5765
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
5862
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id15)).returning();
|
|
5766
5863
|
return result[0] ?? null;
|
|
5767
5864
|
}
|
|
5768
5865
|
/**
|
|
5769
5866
|
* 마지막 로그인 시간 업데이트
|
|
5770
5867
|
* Write primary 사용
|
|
5771
5868
|
*/
|
|
5772
|
-
async updateLastLogin(
|
|
5869
|
+
async updateLastLogin(id15) {
|
|
5773
5870
|
const result = await this.db.update(users).set({
|
|
5774
5871
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
5775
|
-
}).where(eq(users.id,
|
|
5872
|
+
}).where(eq(users.id, id15)).returning();
|
|
5776
5873
|
return result[0] ?? null;
|
|
5777
5874
|
}
|
|
5778
5875
|
/**
|
|
5779
5876
|
* 사용자 삭제
|
|
5780
5877
|
* Write primary 사용
|
|
5781
5878
|
*/
|
|
5782
|
-
async deleteById(
|
|
5783
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
5879
|
+
async deleteById(id15) {
|
|
5880
|
+
const result = await this.db.delete(users).where(eq(users.id, id15)).returning();
|
|
5784
5881
|
return result[0] ?? null;
|
|
5785
5882
|
}
|
|
5786
5883
|
/**
|
|
@@ -6128,14 +6225,14 @@ var init_keys_repository = __esm({
|
|
|
6128
6225
|
* stored, so it answers "since when has this device been on this release"
|
|
6129
6226
|
* rather than "when was it last seen", which lastUsedAt already answers.
|
|
6130
6227
|
*/
|
|
6131
|
-
async updateLastUsedById(
|
|
6228
|
+
async updateLastUsedById(id15, identity) {
|
|
6132
6229
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
6133
6230
|
const lastUsedIsStale = or(
|
|
6134
6231
|
isNull(userPublicKeys.lastUsedAt),
|
|
6135
6232
|
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
6136
6233
|
);
|
|
6137
6234
|
if (!identity) {
|
|
6138
|
-
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id,
|
|
6235
|
+
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id15), lastUsedIsStale));
|
|
6139
6236
|
return;
|
|
6140
6237
|
}
|
|
6141
6238
|
const identityChanged = sql5`(
|
|
@@ -6152,7 +6249,7 @@ var init_keys_repository = __esm({
|
|
|
6152
6249
|
clientContractVersion: identity.contractVersion,
|
|
6153
6250
|
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
6154
6251
|
}).where(and2(
|
|
6155
|
-
eq2(userPublicKeys.id,
|
|
6252
|
+
eq2(userPublicKeys.id, id15),
|
|
6156
6253
|
or(lastUsedIsStale, identityChanged)
|
|
6157
6254
|
));
|
|
6158
6255
|
}
|
|
@@ -6191,8 +6288,8 @@ var init_verification_codes_repository = __esm({
|
|
|
6191
6288
|
* ID로 인증 코드 조회
|
|
6192
6289
|
* Read replica 사용
|
|
6193
6290
|
*/
|
|
6194
|
-
async findById(
|
|
6195
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
6291
|
+
async findById(id15) {
|
|
6292
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id15)).limit(1);
|
|
6196
6293
|
return result[0] ?? null;
|
|
6197
6294
|
}
|
|
6198
6295
|
/**
|
|
@@ -6206,22 +6303,22 @@ var init_verification_codes_repository = __esm({
|
|
|
6206
6303
|
* 인증 코드 사용 처리
|
|
6207
6304
|
* Write primary 사용
|
|
6208
6305
|
*/
|
|
6209
|
-
async markAsUsed(
|
|
6306
|
+
async markAsUsed(id15) {
|
|
6210
6307
|
const result = await this.db.update(verificationCodes).set({
|
|
6211
6308
|
usedAt: /* @__PURE__ */ new Date()
|
|
6212
|
-
}).where(eq3(verificationCodes.id,
|
|
6309
|
+
}).where(eq3(verificationCodes.id, id15)).returning();
|
|
6213
6310
|
return result[0] ?? null;
|
|
6214
6311
|
}
|
|
6215
6312
|
/**
|
|
6216
6313
|
* 시도 횟수 증가
|
|
6217
6314
|
* Write primary 사용
|
|
6218
6315
|
*/
|
|
6219
|
-
async incrementAttempts(
|
|
6220
|
-
const code = await this.findById(
|
|
6316
|
+
async incrementAttempts(id15) {
|
|
6317
|
+
const code = await this.findById(id15);
|
|
6221
6318
|
if (!code) return null;
|
|
6222
6319
|
const result = await this.db.update(verificationCodes).set({
|
|
6223
6320
|
attempts: code.attempts + 1
|
|
6224
|
-
}).where(eq3(verificationCodes.id,
|
|
6321
|
+
}).where(eq3(verificationCodes.id, id15)).returning();
|
|
6225
6322
|
return result[0] ?? null;
|
|
6226
6323
|
}
|
|
6227
6324
|
/**
|
|
@@ -6309,14 +6406,14 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
6309
6406
|
*
|
|
6310
6407
|
* @returns the updated row, or null if another request claimed it first
|
|
6311
6408
|
*/
|
|
6312
|
-
async claimLink(
|
|
6409
|
+
async claimLink(id15, setupSecretHash, setupExpiresAt) {
|
|
6313
6410
|
const result = await this.db.update(signupLinkTokens).set({
|
|
6314
6411
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
6315
6412
|
setupSecretHash,
|
|
6316
6413
|
setupExpiresAt
|
|
6317
6414
|
}).where(
|
|
6318
6415
|
and4(
|
|
6319
|
-
eq4(signupLinkTokens.id,
|
|
6416
|
+
eq4(signupLinkTokens.id, id15),
|
|
6320
6417
|
isNull3(signupLinkTokens.consumedAt),
|
|
6321
6418
|
isNull3(signupLinkTokens.supersededAt)
|
|
6322
6419
|
)
|
|
@@ -6329,10 +6426,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
6329
6426
|
*
|
|
6330
6427
|
* @returns the updated row, or null if another request completed it first
|
|
6331
6428
|
*/
|
|
6332
|
-
async claimSetupSession(
|
|
6429
|
+
async claimSetupSession(id15) {
|
|
6333
6430
|
const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
6334
6431
|
and4(
|
|
6335
|
-
eq4(signupLinkTokens.id,
|
|
6432
|
+
eq4(signupLinkTokens.id, id15),
|
|
6336
6433
|
isNull3(signupLinkTokens.completedAt),
|
|
6337
6434
|
isNull3(signupLinkTokens.supersededAt)
|
|
6338
6435
|
)
|
|
@@ -6374,27 +6471,172 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
6374
6471
|
}
|
|
6375
6472
|
});
|
|
6376
6473
|
|
|
6377
|
-
// src/server/repositories/
|
|
6474
|
+
// src/server/repositories/device-authorizations.repository.ts
|
|
6378
6475
|
import { BaseRepository as BaseRepository5 } from "@spfn/core/db";
|
|
6379
|
-
import { eq as eq5,
|
|
6476
|
+
import { eq as eq5, and as and5, gt as gt2, inArray, sql as sql6 } from "drizzle-orm";
|
|
6477
|
+
var notExpired, DeviceAuthorizationsRepository, deviceAuthorizationsRepository;
|
|
6478
|
+
var init_device_authorizations_repository = __esm({
|
|
6479
|
+
"src/server/repositories/device-authorizations.repository.ts"() {
|
|
6480
|
+
"use strict";
|
|
6481
|
+
init_device_authorizations();
|
|
6482
|
+
notExpired = () => gt2(deviceAuthorizations.expiresAt, sql6`now()`);
|
|
6483
|
+
DeviceAuthorizationsRepository = class extends BaseRepository5 {
|
|
6484
|
+
/**
|
|
6485
|
+
* Insert a pending authorization, unless one of its codes is already taken.
|
|
6486
|
+
*
|
|
6487
|
+
* `onConflictDoNothing` rather than letting the unique index raise: the start
|
|
6488
|
+
* route runs inside a transaction, and a raised unique violation aborts it,
|
|
6489
|
+
* so the retry that a generated-code collision calls for could not run — every
|
|
6490
|
+
* statement after it would fail with "current transaction is aborted" instead.
|
|
6491
|
+
* An empty result is the collision signal, and the caller draws a fresh code.
|
|
6492
|
+
*
|
|
6493
|
+
* Write primary.
|
|
6494
|
+
*
|
|
6495
|
+
* @returns the inserted row, or null if either code collided
|
|
6496
|
+
*/
|
|
6497
|
+
async create(data) {
|
|
6498
|
+
const result = await this.db.insert(deviceAuthorizations).values(data).onConflictDoNothing().returning();
|
|
6499
|
+
return result[0] ?? null;
|
|
6500
|
+
}
|
|
6501
|
+
/**
|
|
6502
|
+
* Find a record by its normalized user code, in any state.
|
|
6503
|
+
*
|
|
6504
|
+
* Deliberately unfiltered: which refusal a caller is owed — expired, already
|
|
6505
|
+
* handled, unknown — is the service's decision, and a row filtered out here
|
|
6506
|
+
* would be indistinguishable from a code that was never issued.
|
|
6507
|
+
*
|
|
6508
|
+
* Read replica.
|
|
6509
|
+
*/
|
|
6510
|
+
async findByUserCode(userCode) {
|
|
6511
|
+
const result = await this.readDb.select().from(deviceAuthorizations).where(eq5(deviceAuthorizations.userCode, userCode)).limit(1);
|
|
6512
|
+
return result[0] ?? null;
|
|
6513
|
+
}
|
|
6514
|
+
/**
|
|
6515
|
+
* Find a record by the hash of a device code, in any state.
|
|
6516
|
+
* Unfiltered for the same reason as `findByUserCode`.
|
|
6517
|
+
*
|
|
6518
|
+
* Read replica.
|
|
6519
|
+
*/
|
|
6520
|
+
async findByDeviceCodeHash(deviceCodeHash) {
|
|
6521
|
+
const result = await this.readDb.select().from(deviceAuthorizations).where(eq5(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
|
|
6522
|
+
return result[0] ?? null;
|
|
6523
|
+
}
|
|
6524
|
+
/**
|
|
6525
|
+
* Bind the approving user and move the record to `approved`, but only from
|
|
6526
|
+
* `pending`.
|
|
6527
|
+
*
|
|
6528
|
+
* `userId` comes from the approver's authenticated session — never from a
|
|
6529
|
+
* request body — so this is the point where the record gains an owner.
|
|
6530
|
+
*
|
|
6531
|
+
* @returns the updated row, or null if it was no longer pending, or expired
|
|
6532
|
+
*/
|
|
6533
|
+
async approve(id15, userId) {
|
|
6534
|
+
const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
|
|
6535
|
+
and5(
|
|
6536
|
+
eq5(deviceAuthorizations.id, id15),
|
|
6537
|
+
eq5(deviceAuthorizations.status, "pending"),
|
|
6538
|
+
notExpired()
|
|
6539
|
+
)
|
|
6540
|
+
).returning();
|
|
6541
|
+
return result[0] ?? null;
|
|
6542
|
+
}
|
|
6543
|
+
/**
|
|
6544
|
+
* Move the record to `denied`, but only from `pending`.
|
|
6545
|
+
*
|
|
6546
|
+
* No user is bound: a refusal should leave no record of who was asked.
|
|
6547
|
+
*
|
|
6548
|
+
* @returns the updated row, or null if it was no longer pending, or expired
|
|
6549
|
+
*/
|
|
6550
|
+
async deny(id15) {
|
|
6551
|
+
const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
6552
|
+
and5(
|
|
6553
|
+
eq5(deviceAuthorizations.id, id15),
|
|
6554
|
+
eq5(deviceAuthorizations.status, "pending"),
|
|
6555
|
+
notExpired()
|
|
6556
|
+
)
|
|
6557
|
+
).returning();
|
|
6558
|
+
return result[0] ?? null;
|
|
6559
|
+
}
|
|
6560
|
+
/**
|
|
6561
|
+
* Refuse every authorization a user still has in flight.
|
|
6562
|
+
*
|
|
6563
|
+
* This is the device-code half of a global revocation. Revoking
|
|
6564
|
+
* `user_public_keys` alone leaves an approved-but-uncollected record behind,
|
|
6565
|
+
* and the next poll on it registers a brand-new active key — so "sign every
|
|
6566
|
+
* device out" would hand one straight back to whoever was still waiting,
|
|
6567
|
+
* which is exactly the person a revoke-all is usually aimed at.
|
|
6568
|
+
*
|
|
6569
|
+
* `denied` rather than a status of its own: a swept record owes its holder
|
|
6570
|
+
* the answer a refused one owes, on all four operations — the waiting device
|
|
6571
|
+
* is told no and stops polling, and info/approve/deny say the request was
|
|
6572
|
+
* already answered. Adding a fourth live status would add a migration and a
|
|
6573
|
+
* case-table row to record a distinction nothing acts on.
|
|
6574
|
+
*
|
|
6575
|
+
* Expired rows are swept too. A global revoke is not the place to reason
|
|
6576
|
+
* about which dead rows were about to die anyway.
|
|
6577
|
+
*
|
|
6578
|
+
* `pending` alongside `approved` even though a pending row carries no
|
|
6579
|
+
* `userId` today, so only approved rows can match: the state list says which
|
|
6580
|
+
* states this is meant to close, and a later change that binds the user
|
|
6581
|
+
* earlier should not silently reopen the hole.
|
|
6582
|
+
*
|
|
6583
|
+
* @returns the rows this call refused
|
|
6584
|
+
*/
|
|
6585
|
+
async denyAllActiveByUserId(userId) {
|
|
6586
|
+
return await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
6587
|
+
and5(
|
|
6588
|
+
eq5(deviceAuthorizations.userId, userId),
|
|
6589
|
+
inArray(deviceAuthorizations.status, ["pending", "approved"])
|
|
6590
|
+
)
|
|
6591
|
+
).returning();
|
|
6592
|
+
}
|
|
6593
|
+
/**
|
|
6594
|
+
* Spend an approved record, but only from `approved`, and address it by the
|
|
6595
|
+
* device code hash the caller actually presented.
|
|
6596
|
+
*
|
|
6597
|
+
* This is the one-shot: the winner of two concurrent polls registers the key,
|
|
6598
|
+
* and the loser sees no approved record and is answered as if the code were
|
|
6599
|
+
* unknown. Matching on the hash rather than on an id read a moment ago keeps
|
|
6600
|
+
* the whole decision in one statement.
|
|
6601
|
+
*
|
|
6602
|
+
* @returns the spent row, or null if it was not approved (any more), or expired
|
|
6603
|
+
*/
|
|
6604
|
+
async consumeApproved(deviceCodeHash) {
|
|
6605
|
+
const result = await this.db.update(deviceAuthorizations).set({ status: "consumed", consumedAt: /* @__PURE__ */ new Date() }).where(
|
|
6606
|
+
and5(
|
|
6607
|
+
eq5(deviceAuthorizations.deviceCodeHash, deviceCodeHash),
|
|
6608
|
+
eq5(deviceAuthorizations.status, "approved"),
|
|
6609
|
+
notExpired()
|
|
6610
|
+
)
|
|
6611
|
+
).returning();
|
|
6612
|
+
return result[0] ?? null;
|
|
6613
|
+
}
|
|
6614
|
+
};
|
|
6615
|
+
deviceAuthorizationsRepository = new DeviceAuthorizationsRepository();
|
|
6616
|
+
}
|
|
6617
|
+
});
|
|
6618
|
+
|
|
6619
|
+
// src/server/repositories/roles.repository.ts
|
|
6620
|
+
import { BaseRepository as BaseRepository6 } from "@spfn/core/db";
|
|
6621
|
+
import { eq as eq6, asc } from "drizzle-orm";
|
|
6380
6622
|
var RolesRepository, rolesRepository;
|
|
6381
6623
|
var init_roles_repository = __esm({
|
|
6382
6624
|
"src/server/repositories/roles.repository.ts"() {
|
|
6383
6625
|
"use strict";
|
|
6384
6626
|
init_roles();
|
|
6385
|
-
RolesRepository = class extends
|
|
6627
|
+
RolesRepository = class extends BaseRepository6 {
|
|
6386
6628
|
/**
|
|
6387
6629
|
* ID로 역할 조회
|
|
6388
6630
|
*/
|
|
6389
|
-
async findById(
|
|
6390
|
-
const result = await this.readDb.select().from(roles).where(
|
|
6631
|
+
async findById(id15) {
|
|
6632
|
+
const result = await this.readDb.select().from(roles).where(eq6(roles.id, id15)).limit(1);
|
|
6391
6633
|
return result[0] ?? null;
|
|
6392
6634
|
}
|
|
6393
6635
|
/**
|
|
6394
6636
|
* Name으로 역할 조회
|
|
6395
6637
|
*/
|
|
6396
6638
|
async findByName(name) {
|
|
6397
|
-
const result = await this.readDb.select().from(roles).where(
|
|
6639
|
+
const result = await this.readDb.select().from(roles).where(eq6(roles.name, name)).limit(1);
|
|
6398
6640
|
return result[0] ?? null;
|
|
6399
6641
|
}
|
|
6400
6642
|
/**
|
|
@@ -6407,7 +6649,7 @@ var init_roles_repository = __esm({
|
|
|
6407
6649
|
* 활성 역할만 조회
|
|
6408
6650
|
*/
|
|
6409
6651
|
async findActive() {
|
|
6410
|
-
return this.readDb.select().from(roles).where(
|
|
6652
|
+
return this.readDb.select().from(roles).where(eq6(roles.isActive, true)).orderBy(asc(roles.priority));
|
|
6411
6653
|
}
|
|
6412
6654
|
/**
|
|
6413
6655
|
* 역할 생성
|
|
@@ -6418,15 +6660,15 @@ var init_roles_repository = __esm({
|
|
|
6418
6660
|
/**
|
|
6419
6661
|
* 역할 업데이트
|
|
6420
6662
|
*/
|
|
6421
|
-
async updateById(
|
|
6422
|
-
const result = await this.db.update(roles).set(data).where(
|
|
6663
|
+
async updateById(id15, data) {
|
|
6664
|
+
const result = await this.db.update(roles).set(data).where(eq6(roles.id, id15)).returning();
|
|
6423
6665
|
return result[0] ?? null;
|
|
6424
6666
|
}
|
|
6425
6667
|
/**
|
|
6426
6668
|
* 역할 삭제
|
|
6427
6669
|
*/
|
|
6428
|
-
async deleteById(
|
|
6429
|
-
const result = await this.db.delete(roles).where(
|
|
6670
|
+
async deleteById(id15) {
|
|
6671
|
+
const result = await this.db.delete(roles).where(eq6(roles.id, id15)).returning();
|
|
6430
6672
|
return result[0] ?? null;
|
|
6431
6673
|
}
|
|
6432
6674
|
};
|
|
@@ -6435,26 +6677,26 @@ var init_roles_repository = __esm({
|
|
|
6435
6677
|
});
|
|
6436
6678
|
|
|
6437
6679
|
// src/server/repositories/permissions.repository.ts
|
|
6438
|
-
import { BaseRepository as
|
|
6439
|
-
import { asc as asc2, eq as
|
|
6680
|
+
import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
|
|
6681
|
+
import { asc as asc2, eq as eq7, inArray as inArray2 } from "drizzle-orm";
|
|
6440
6682
|
var PermissionsRepository, permissionsRepository;
|
|
6441
6683
|
var init_permissions_repository = __esm({
|
|
6442
6684
|
"src/server/repositories/permissions.repository.ts"() {
|
|
6443
6685
|
"use strict";
|
|
6444
6686
|
init_permissions();
|
|
6445
|
-
PermissionsRepository = class extends
|
|
6687
|
+
PermissionsRepository = class extends BaseRepository7 {
|
|
6446
6688
|
/**
|
|
6447
6689
|
* ID로 권한 조회
|
|
6448
6690
|
*/
|
|
6449
|
-
async findById(
|
|
6450
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
6691
|
+
async findById(id15) {
|
|
6692
|
+
const result = await this.readDb.select().from(permissions).where(eq7(permissions.id, id15)).limit(1);
|
|
6451
6693
|
return result[0] ?? null;
|
|
6452
6694
|
}
|
|
6453
6695
|
/**
|
|
6454
6696
|
* Name으로 권한 조회
|
|
6455
6697
|
*/
|
|
6456
6698
|
async findByName(name) {
|
|
6457
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
6699
|
+
const result = await this.readDb.select().from(permissions).where(eq7(permissions.name, name)).limit(1);
|
|
6458
6700
|
return result[0] ?? null;
|
|
6459
6701
|
}
|
|
6460
6702
|
/**
|
|
@@ -6462,7 +6704,7 @@ var init_permissions_repository = __esm({
|
|
|
6462
6704
|
*/
|
|
6463
6705
|
async findByNames(names) {
|
|
6464
6706
|
if (names.length === 0) return [];
|
|
6465
|
-
return this.readDb.select().from(permissions).where(
|
|
6707
|
+
return this.readDb.select().from(permissions).where(inArray2(permissions.name, names));
|
|
6466
6708
|
}
|
|
6467
6709
|
/**
|
|
6468
6710
|
* 모든 권한 조회
|
|
@@ -6474,13 +6716,13 @@ var init_permissions_repository = __esm({
|
|
|
6474
6716
|
* 활성 권한만 조회
|
|
6475
6717
|
*/
|
|
6476
6718
|
async findActive() {
|
|
6477
|
-
return this.readDb.select().from(permissions).where(
|
|
6719
|
+
return this.readDb.select().from(permissions).where(eq7(permissions.isActive, true)).orderBy(asc2(permissions.name));
|
|
6478
6720
|
}
|
|
6479
6721
|
/**
|
|
6480
6722
|
* 카테고리별 권한 조회
|
|
6481
6723
|
*/
|
|
6482
6724
|
async findByCategory(category) {
|
|
6483
|
-
return this.readDb.select().from(permissions).where(
|
|
6725
|
+
return this.readDb.select().from(permissions).where(eq7(permissions.category, category)).orderBy(asc2(permissions.name));
|
|
6484
6726
|
}
|
|
6485
6727
|
/**
|
|
6486
6728
|
* 권한 생성
|
|
@@ -6498,15 +6740,15 @@ var init_permissions_repository = __esm({
|
|
|
6498
6740
|
/**
|
|
6499
6741
|
* 권한 업데이트
|
|
6500
6742
|
*/
|
|
6501
|
-
async updateById(
|
|
6502
|
-
const result = await this.db.update(permissions).set(data).where(
|
|
6743
|
+
async updateById(id15, data) {
|
|
6744
|
+
const result = await this.db.update(permissions).set(data).where(eq7(permissions.id, id15)).returning();
|
|
6503
6745
|
return result[0] ?? null;
|
|
6504
6746
|
}
|
|
6505
6747
|
/**
|
|
6506
6748
|
* 권한 삭제
|
|
6507
6749
|
*/
|
|
6508
|
-
async deleteById(
|
|
6509
|
-
const result = await this.db.delete(permissions).where(
|
|
6750
|
+
async deleteById(id15) {
|
|
6751
|
+
const result = await this.db.delete(permissions).where(eq7(permissions.id, id15)).returning();
|
|
6510
6752
|
return result[0] ?? null;
|
|
6511
6753
|
}
|
|
6512
6754
|
};
|
|
@@ -6515,25 +6757,25 @@ var init_permissions_repository = __esm({
|
|
|
6515
6757
|
});
|
|
6516
6758
|
|
|
6517
6759
|
// src/server/repositories/role-permissions.repository.ts
|
|
6518
|
-
import { BaseRepository as
|
|
6519
|
-
import { and as
|
|
6760
|
+
import { BaseRepository as BaseRepository8 } from "@spfn/core/db";
|
|
6761
|
+
import { and as and6, eq as eq8 } from "drizzle-orm";
|
|
6520
6762
|
var RolePermissionsRepository, rolePermissionsRepository;
|
|
6521
6763
|
var init_role_permissions_repository = __esm({
|
|
6522
6764
|
"src/server/repositories/role-permissions.repository.ts"() {
|
|
6523
6765
|
"use strict";
|
|
6524
6766
|
init_role_permissions();
|
|
6525
|
-
RolePermissionsRepository = class extends
|
|
6767
|
+
RolePermissionsRepository = class extends BaseRepository8 {
|
|
6526
6768
|
/**
|
|
6527
6769
|
* 역할 ID로 모든 권한 조회
|
|
6528
6770
|
*/
|
|
6529
6771
|
async findByRoleId(roleId) {
|
|
6530
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
6772
|
+
return this.readDb.select().from(rolePermissions).where(eq8(rolePermissions.roleId, roleId));
|
|
6531
6773
|
}
|
|
6532
6774
|
/**
|
|
6533
6775
|
* 권한 ID로 모든 역할 조회
|
|
6534
6776
|
*/
|
|
6535
6777
|
async findByPermissionId(permissionId) {
|
|
6536
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
6778
|
+
return this.readDb.select().from(rolePermissions).where(eq8(rolePermissions.permissionId, permissionId));
|
|
6537
6779
|
}
|
|
6538
6780
|
/**
|
|
6539
6781
|
* 역할-권한 매핑 생성
|
|
@@ -6553,9 +6795,9 @@ var init_role_permissions_repository = __esm({
|
|
|
6553
6795
|
*/
|
|
6554
6796
|
async deleteByRoleIdAndPermissionId(roleId, permissionId) {
|
|
6555
6797
|
const result = await this.db.delete(rolePermissions).where(
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6798
|
+
and6(
|
|
6799
|
+
eq8(rolePermissions.roleId, roleId),
|
|
6800
|
+
eq8(rolePermissions.permissionId, permissionId)
|
|
6559
6801
|
)
|
|
6560
6802
|
).returning();
|
|
6561
6803
|
return result[0] ?? null;
|
|
@@ -6564,7 +6806,7 @@ var init_role_permissions_repository = __esm({
|
|
|
6564
6806
|
* 역할의 모든 권한 매핑 삭제
|
|
6565
6807
|
*/
|
|
6566
6808
|
async deleteByRoleId(roleId) {
|
|
6567
|
-
const result = await this.db.delete(rolePermissions).where(
|
|
6809
|
+
const result = await this.db.delete(rolePermissions).where(eq8(rolePermissions.roleId, roleId)).returning();
|
|
6568
6810
|
return result.length;
|
|
6569
6811
|
}
|
|
6570
6812
|
/**
|
|
@@ -6585,19 +6827,19 @@ var init_role_permissions_repository = __esm({
|
|
|
6585
6827
|
});
|
|
6586
6828
|
|
|
6587
6829
|
// src/server/repositories/user-permissions.repository.ts
|
|
6588
|
-
import { BaseRepository as
|
|
6589
|
-
import { eq as
|
|
6830
|
+
import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
|
|
6831
|
+
import { eq as eq9, and as and7, or as or2, isNull as isNull4, isNotNull, lt as lt3, gt as gt3 } from "drizzle-orm";
|
|
6590
6832
|
var UserPermissionsRepository, userPermissionsRepository;
|
|
6591
6833
|
var init_user_permissions_repository = __esm({
|
|
6592
6834
|
"src/server/repositories/user-permissions.repository.ts"() {
|
|
6593
6835
|
"use strict";
|
|
6594
6836
|
init_user_permissions();
|
|
6595
|
-
UserPermissionsRepository = class extends
|
|
6837
|
+
UserPermissionsRepository = class extends BaseRepository9 {
|
|
6596
6838
|
/**
|
|
6597
6839
|
* 사용자 ID로 모든 권한 오버라이드 조회
|
|
6598
6840
|
*/
|
|
6599
6841
|
async findByUserId(userId) {
|
|
6600
|
-
return this.readDb.select().from(userPermissions).where(
|
|
6842
|
+
return this.readDb.select().from(userPermissions).where(eq9(userPermissions.userId, userId));
|
|
6601
6843
|
}
|
|
6602
6844
|
/**
|
|
6603
6845
|
* 사용자 ID로 유효한 권한 오버라이드만 조회
|
|
@@ -6606,11 +6848,11 @@ var init_user_permissions_repository = __esm({
|
|
|
6606
6848
|
async findValidByUserId(userId) {
|
|
6607
6849
|
const now = /* @__PURE__ */ new Date();
|
|
6608
6850
|
return this.readDb.select().from(userPermissions).where(
|
|
6609
|
-
|
|
6610
|
-
|
|
6851
|
+
and7(
|
|
6852
|
+
eq9(userPermissions.userId, userId),
|
|
6611
6853
|
or2(
|
|
6612
6854
|
isNull4(userPermissions.expiresAt),
|
|
6613
|
-
|
|
6855
|
+
gt3(userPermissions.expiresAt, now)
|
|
6614
6856
|
)
|
|
6615
6857
|
)
|
|
6616
6858
|
);
|
|
@@ -6620,9 +6862,9 @@ var init_user_permissions_repository = __esm({
|
|
|
6620
6862
|
*/
|
|
6621
6863
|
async findByUserIdAndPermissionId(userId, permissionId) {
|
|
6622
6864
|
const result = await this.readDb.select().from(userPermissions).where(
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6865
|
+
and7(
|
|
6866
|
+
eq9(userPermissions.userId, userId),
|
|
6867
|
+
eq9(userPermissions.permissionId, permissionId)
|
|
6626
6868
|
)
|
|
6627
6869
|
).limit(1);
|
|
6628
6870
|
return result[0] ?? null;
|
|
@@ -6636,8 +6878,8 @@ var init_user_permissions_repository = __esm({
|
|
|
6636
6878
|
/**
|
|
6637
6879
|
* 사용자 권한 오버라이드 업데이트
|
|
6638
6880
|
*/
|
|
6639
|
-
async updateById(
|
|
6640
|
-
const result = await this.db.update(userPermissions).set(data).where(
|
|
6881
|
+
async updateById(id15, data) {
|
|
6882
|
+
const result = await this.db.update(userPermissions).set(data).where(eq9(userPermissions.id, id15)).returning();
|
|
6641
6883
|
return result[0] ?? null;
|
|
6642
6884
|
}
|
|
6643
6885
|
/**
|
|
@@ -6645,9 +6887,9 @@ var init_user_permissions_repository = __esm({
|
|
|
6645
6887
|
*/
|
|
6646
6888
|
async deleteByUserIdAndPermissionId(userId, permissionId) {
|
|
6647
6889
|
const result = await this.db.delete(userPermissions).where(
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6890
|
+
and7(
|
|
6891
|
+
eq9(userPermissions.userId, userId),
|
|
6892
|
+
eq9(userPermissions.permissionId, permissionId)
|
|
6651
6893
|
)
|
|
6652
6894
|
).returning();
|
|
6653
6895
|
return result[0] ?? null;
|
|
@@ -6656,7 +6898,7 @@ var init_user_permissions_repository = __esm({
|
|
|
6656
6898
|
* 사용자의 모든 권한 오버라이드 삭제
|
|
6657
6899
|
*/
|
|
6658
6900
|
async deleteByUserId(userId) {
|
|
6659
|
-
const result = await this.db.delete(userPermissions).where(
|
|
6901
|
+
const result = await this.db.delete(userPermissions).where(eq9(userPermissions.userId, userId)).returning();
|
|
6660
6902
|
return result.length;
|
|
6661
6903
|
}
|
|
6662
6904
|
/**
|
|
@@ -6665,7 +6907,7 @@ var init_user_permissions_repository = __esm({
|
|
|
6665
6907
|
async deleteExpired() {
|
|
6666
6908
|
const now = /* @__PURE__ */ new Date();
|
|
6667
6909
|
const result = await this.db.delete(userPermissions).where(
|
|
6668
|
-
|
|
6910
|
+
and7(
|
|
6669
6911
|
isNotNull(userPermissions.expiresAt),
|
|
6670
6912
|
lt3(userPermissions.expiresAt, now)
|
|
6671
6913
|
)
|
|
@@ -6678,33 +6920,33 @@ var init_user_permissions_repository = __esm({
|
|
|
6678
6920
|
});
|
|
6679
6921
|
|
|
6680
6922
|
// src/server/repositories/user-profiles.repository.ts
|
|
6681
|
-
import { BaseRepository as
|
|
6682
|
-
import { eq as
|
|
6923
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6924
|
+
import { eq as eq10 } from "drizzle-orm";
|
|
6683
6925
|
var UserProfilesRepository, userProfilesRepository;
|
|
6684
6926
|
var init_user_profiles_repository = __esm({
|
|
6685
6927
|
"src/server/repositories/user-profiles.repository.ts"() {
|
|
6686
6928
|
"use strict";
|
|
6687
6929
|
init_user_profiles();
|
|
6688
|
-
UserProfilesRepository = class extends
|
|
6930
|
+
UserProfilesRepository = class extends BaseRepository10 {
|
|
6689
6931
|
/**
|
|
6690
6932
|
* ID로 프로필 조회
|
|
6691
6933
|
*/
|
|
6692
|
-
async findById(
|
|
6693
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
6934
|
+
async findById(id15) {
|
|
6935
|
+
const result = await this.readDb.select().from(userProfiles).where(eq10(userProfiles.id, id15)).limit(1);
|
|
6694
6936
|
return result[0] ?? null;
|
|
6695
6937
|
}
|
|
6696
6938
|
/**
|
|
6697
6939
|
* User ID로 locale만 조회 (경량)
|
|
6698
6940
|
*/
|
|
6699
6941
|
async findLocaleByUserId(userId) {
|
|
6700
|
-
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(
|
|
6942
|
+
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq10(userProfiles.userId, userId)).limit(1);
|
|
6701
6943
|
return result[0]?.locale || "en";
|
|
6702
6944
|
}
|
|
6703
6945
|
/**
|
|
6704
6946
|
* User ID로 프로필 조회
|
|
6705
6947
|
*/
|
|
6706
6948
|
async findByUserId(userId) {
|
|
6707
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
6949
|
+
const result = await this.readDb.select().from(userProfiles).where(eq10(userProfiles.userId, userId)).limit(1);
|
|
6708
6950
|
return result[0] ?? null;
|
|
6709
6951
|
}
|
|
6710
6952
|
/**
|
|
@@ -6716,29 +6958,29 @@ var init_user_profiles_repository = __esm({
|
|
|
6716
6958
|
/**
|
|
6717
6959
|
* 프로필 업데이트 (by ID)
|
|
6718
6960
|
*/
|
|
6719
|
-
async updateById(
|
|
6720
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
6961
|
+
async updateById(id15, data) {
|
|
6962
|
+
const result = await this.db.update(userProfiles).set(data).where(eq10(userProfiles.id, id15)).returning();
|
|
6721
6963
|
return result[0] ?? null;
|
|
6722
6964
|
}
|
|
6723
6965
|
/**
|
|
6724
6966
|
* 프로필 업데이트 (by User ID)
|
|
6725
6967
|
*/
|
|
6726
6968
|
async updateByUserId(userId, data) {
|
|
6727
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
6969
|
+
const result = await this.db.update(userProfiles).set(data).where(eq10(userProfiles.userId, userId)).returning();
|
|
6728
6970
|
return result[0] ?? null;
|
|
6729
6971
|
}
|
|
6730
6972
|
/**
|
|
6731
6973
|
* 프로필 삭제 (by ID)
|
|
6732
6974
|
*/
|
|
6733
|
-
async deleteById(
|
|
6734
|
-
const result = await this.db.delete(userProfiles).where(
|
|
6975
|
+
async deleteById(id15) {
|
|
6976
|
+
const result = await this.db.delete(userProfiles).where(eq10(userProfiles.id, id15)).returning();
|
|
6735
6977
|
return result[0] ?? null;
|
|
6736
6978
|
}
|
|
6737
6979
|
/**
|
|
6738
6980
|
* 프로필 삭제 (by User ID)
|
|
6739
6981
|
*/
|
|
6740
6982
|
async deleteByUserId(userId) {
|
|
6741
|
-
const result = await this.db.delete(userProfiles).where(
|
|
6983
|
+
const result = await this.db.delete(userProfiles).where(eq10(userProfiles.userId, userId)).returning();
|
|
6742
6984
|
return result[0] ?? null;
|
|
6743
6985
|
}
|
|
6744
6986
|
/**
|
|
@@ -6780,7 +7022,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6780
7022
|
metadata: userProfiles.metadata,
|
|
6781
7023
|
createdAt: userProfiles.createdAt,
|
|
6782
7024
|
updatedAt: userProfiles.updatedAt
|
|
6783
|
-
}).from(userProfiles).where(
|
|
7025
|
+
}).from(userProfiles).where(eq10(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
6784
7026
|
if (!profile) {
|
|
6785
7027
|
return null;
|
|
6786
7028
|
}
|
|
@@ -6808,8 +7050,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6808
7050
|
});
|
|
6809
7051
|
|
|
6810
7052
|
// src/server/repositories/invitations.repository.ts
|
|
6811
|
-
import { eq as
|
|
6812
|
-
import { BaseRepository as
|
|
7053
|
+
import { eq as eq11, and as and8, lt as lt4, desc as desc2, sql as sql7 } from "drizzle-orm";
|
|
7054
|
+
import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
|
|
6813
7055
|
var InvitationsRepository, invitationsRepository;
|
|
6814
7056
|
var init_invitations_repository = __esm({
|
|
6815
7057
|
"src/server/repositories/invitations.repository.ts"() {
|
|
@@ -6818,19 +7060,19 @@ var init_invitations_repository = __esm({
|
|
|
6818
7060
|
init_roles();
|
|
6819
7061
|
init_user_invitations();
|
|
6820
7062
|
init_email();
|
|
6821
|
-
InvitationsRepository = class extends
|
|
7063
|
+
InvitationsRepository = class extends BaseRepository11 {
|
|
6822
7064
|
/**
|
|
6823
7065
|
* ID로 초대 조회
|
|
6824
7066
|
*/
|
|
6825
|
-
async findById(
|
|
6826
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
7067
|
+
async findById(id15) {
|
|
7068
|
+
const result = await this.readDb.select().from(userInvitations).where(eq11(userInvitations.id, id15)).limit(1);
|
|
6827
7069
|
return result[0] ?? null;
|
|
6828
7070
|
}
|
|
6829
7071
|
/**
|
|
6830
7072
|
* Token으로 초대 조회
|
|
6831
7073
|
*/
|
|
6832
7074
|
async findByToken(token) {
|
|
6833
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
7075
|
+
const result = await this.readDb.select().from(userInvitations).where(eq11(userInvitations.token, token)).limit(1);
|
|
6834
7076
|
return result[0] ?? null;
|
|
6835
7077
|
}
|
|
6836
7078
|
/**
|
|
@@ -6838,9 +7080,9 @@ var init_invitations_repository = __esm({
|
|
|
6838
7080
|
*/
|
|
6839
7081
|
async findPendingByEmail(email) {
|
|
6840
7082
|
const result = await this.readDb.select().from(userInvitations).where(
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
7083
|
+
and8(
|
|
7084
|
+
eq11(userInvitations.email, normalizeEmail(email)),
|
|
7085
|
+
eq11(userInvitations.status, "pending")
|
|
6844
7086
|
)
|
|
6845
7087
|
).limit(1);
|
|
6846
7088
|
return result[0] ?? null;
|
|
@@ -6849,13 +7091,13 @@ var init_invitations_repository = __esm({
|
|
|
6849
7091
|
* 초대자 ID로 모든 초대 조회
|
|
6850
7092
|
*/
|
|
6851
7093
|
async findByInvitedBy(invitedBy) {
|
|
6852
|
-
return this.readDb.select().from(userInvitations).where(
|
|
7094
|
+
return this.readDb.select().from(userInvitations).where(eq11(userInvitations.invitedBy, invitedBy));
|
|
6853
7095
|
}
|
|
6854
7096
|
/**
|
|
6855
7097
|
* 상태별 초대 조회
|
|
6856
7098
|
*/
|
|
6857
7099
|
async findByStatus(status) {
|
|
6858
|
-
return this.readDb.select().from(userInvitations).where(
|
|
7100
|
+
return this.readDb.select().from(userInvitations).where(eq11(userInvitations.status, status));
|
|
6859
7101
|
}
|
|
6860
7102
|
/**
|
|
6861
7103
|
* 초대 생성
|
|
@@ -6866,7 +7108,7 @@ var init_invitations_repository = __esm({
|
|
|
6866
7108
|
/**
|
|
6867
7109
|
* 초대 상태 업데이트
|
|
6868
7110
|
*/
|
|
6869
|
-
async updateStatus(
|
|
7111
|
+
async updateStatus(id15, status, timestamp2) {
|
|
6870
7112
|
const updates = {
|
|
6871
7113
|
status
|
|
6872
7114
|
};
|
|
@@ -6877,14 +7119,14 @@ var init_invitations_repository = __esm({
|
|
|
6877
7119
|
updates.cancelledAt = timestamp2;
|
|
6878
7120
|
}
|
|
6879
7121
|
}
|
|
6880
|
-
const result = await this.db.update(userInvitations).set(updates).where(
|
|
7122
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq11(userInvitations.id, id15)).returning();
|
|
6881
7123
|
return result[0] ?? null;
|
|
6882
7124
|
}
|
|
6883
7125
|
/**
|
|
6884
7126
|
* 초대 삭제
|
|
6885
7127
|
*/
|
|
6886
|
-
async deleteById(
|
|
6887
|
-
const result = await this.db.delete(userInvitations).where(
|
|
7128
|
+
async deleteById(id15) {
|
|
7129
|
+
const result = await this.db.delete(userInvitations).where(eq11(userInvitations.id, id15)).returning();
|
|
6888
7130
|
return result[0] ?? null;
|
|
6889
7131
|
}
|
|
6890
7132
|
/**
|
|
@@ -6893,8 +7135,8 @@ var init_invitations_repository = __esm({
|
|
|
6893
7135
|
async updateExpiredInvitations() {
|
|
6894
7136
|
const now = /* @__PURE__ */ new Date();
|
|
6895
7137
|
const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
|
|
6896
|
-
|
|
6897
|
-
|
|
7138
|
+
and8(
|
|
7139
|
+
eq11(userInvitations.status, "pending"),
|
|
6898
7140
|
lt4(userInvitations.expiresAt, now)
|
|
6899
7141
|
)
|
|
6900
7142
|
).returning();
|
|
@@ -6926,7 +7168,7 @@ var init_invitations_repository = __esm({
|
|
|
6926
7168
|
id: users.id,
|
|
6927
7169
|
email: users.email
|
|
6928
7170
|
}
|
|
6929
|
-
}).from(userInvitations).innerJoin(roles,
|
|
7171
|
+
}).from(userInvitations).innerJoin(roles, eq11(userInvitations.roleId, roles.id)).innerJoin(users, eq11(userInvitations.invitedBy, users.id)).where(eq11(userInvitations.token, token)).limit(1);
|
|
6930
7172
|
return result[0] ?? null;
|
|
6931
7173
|
}
|
|
6932
7174
|
/**
|
|
@@ -6937,13 +7179,13 @@ var init_invitations_repository = __esm({
|
|
|
6937
7179
|
const offset = (page - 1) * limit;
|
|
6938
7180
|
const conditions = [];
|
|
6939
7181
|
if (status) {
|
|
6940
|
-
conditions.push(
|
|
7182
|
+
conditions.push(eq11(userInvitations.status, status));
|
|
6941
7183
|
}
|
|
6942
7184
|
if (invitedBy) {
|
|
6943
|
-
conditions.push(
|
|
7185
|
+
conditions.push(eq11(userInvitations.invitedBy, invitedBy));
|
|
6944
7186
|
}
|
|
6945
|
-
const whereClause = conditions.length > 0 ?
|
|
6946
|
-
const countResult = await this.readDb.select({ count:
|
|
7187
|
+
const whereClause = conditions.length > 0 ? and8(...conditions) : void 0;
|
|
7188
|
+
const countResult = await this.readDb.select({ count: sql7`count(*)` }).from(userInvitations).where(whereClause);
|
|
6947
7189
|
const total = Number(countResult[0]?.count || 0);
|
|
6948
7190
|
const results = await this.readDb.select({
|
|
6949
7191
|
id: userInvitations.id,
|
|
@@ -6967,7 +7209,7 @@ var init_invitations_repository = __esm({
|
|
|
6967
7209
|
id: users.id,
|
|
6968
7210
|
email: users.email
|
|
6969
7211
|
}
|
|
6970
|
-
}).from(userInvitations).innerJoin(roles,
|
|
7212
|
+
}).from(userInvitations).innerJoin(roles, eq11(userInvitations.roleId, roles.id)).innerJoin(users, eq11(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
|
|
6971
7213
|
return {
|
|
6972
7214
|
invitations: results,
|
|
6973
7215
|
total,
|
|
@@ -6979,31 +7221,31 @@ var init_invitations_repository = __esm({
|
|
|
6979
7221
|
/**
|
|
6980
7222
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
6981
7223
|
*/
|
|
6982
|
-
async updateById(
|
|
7224
|
+
async updateById(id15, data) {
|
|
6983
7225
|
const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
|
|
6984
|
-
const result = await this.db.update(userInvitations).set(patch).where(
|
|
7226
|
+
const result = await this.db.update(userInvitations).set(patch).where(eq11(userInvitations.id, id15)).returning();
|
|
6985
7227
|
return result[0] ?? null;
|
|
6986
7228
|
}
|
|
6987
7229
|
/**
|
|
6988
7230
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
6989
7231
|
*/
|
|
6990
|
-
async resend(
|
|
7232
|
+
async resend(id15, newExpiresAt) {
|
|
6991
7233
|
const result = await this.db.update(userInvitations).set({
|
|
6992
7234
|
status: "pending",
|
|
6993
7235
|
expiresAt: newExpiresAt
|
|
6994
|
-
}).where(
|
|
7236
|
+
}).where(eq11(userInvitations.id, id15)).returning();
|
|
6995
7237
|
return result[0] ?? null;
|
|
6996
7238
|
}
|
|
6997
7239
|
/**
|
|
6998
7240
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
6999
7241
|
*/
|
|
7000
|
-
async cancel(
|
|
7242
|
+
async cancel(id15, cancelledBy, reason, currentMetadata) {
|
|
7001
7243
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
7002
7244
|
const result = await this.db.update(userInvitations).set({
|
|
7003
7245
|
status: "cancelled",
|
|
7004
7246
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
7005
7247
|
metadata: newMetadata
|
|
7006
|
-
}).where(
|
|
7248
|
+
}).where(eq11(userInvitations.id, id15)).returning();
|
|
7007
7249
|
return result[0] ?? null;
|
|
7008
7250
|
}
|
|
7009
7251
|
};
|
|
@@ -7090,6 +7332,15 @@ var init_schema5 = __esm({
|
|
|
7090
7332
|
examples: [true, false]
|
|
7091
7333
|
})
|
|
7092
7334
|
},
|
|
7335
|
+
SPFN_AUTH_CSRF: {
|
|
7336
|
+
...envString({
|
|
7337
|
+
description: 'CSRF protection for cookie-session mutations in the Next.js proxy: off | warn | enforce. Unset behaves as "warn" (log what would be refused, allow it through). configureAuth({ csrf: { mode } }) takes precedence.',
|
|
7338
|
+
required: false,
|
|
7339
|
+
nextjs: true,
|
|
7340
|
+
// The check runs in the Next.js proxy
|
|
7341
|
+
examples: ["enforce", "warn", "off"]
|
|
7342
|
+
})
|
|
7343
|
+
},
|
|
7093
7344
|
SPFN_AUTH_BCRYPT_SALT_ROUNDS: {
|
|
7094
7345
|
...envNumber({
|
|
7095
7346
|
description: "Bcrypt salt rounds (cost factor, higher = more secure but slower)",
|
|
@@ -7317,7 +7568,7 @@ var init_schema5 = __esm({
|
|
|
7317
7568
|
},
|
|
7318
7569
|
SPFN_AUTH_GOOGLE_REDIRECT_URI: {
|
|
7319
7570
|
...envString({
|
|
7320
|
-
description: "Google OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/google/callback
|
|
7571
|
+
description: "Google OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/google/callback. The override must stay on the web app origin at this exact path \u2014 the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.",
|
|
7321
7572
|
required: false,
|
|
7322
7573
|
examples: [
|
|
7323
7574
|
"https://app.example.com/_auth/oauth/google/callback",
|
|
@@ -7360,7 +7611,7 @@ var init_schema5 = __esm({
|
|
|
7360
7611
|
},
|
|
7361
7612
|
SPFN_AUTH_KAKAO_REDIRECT_URI: {
|
|
7362
7613
|
...envString({
|
|
7363
|
-
description: "Kakao OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/kakao/callback.",
|
|
7614
|
+
description: "Kakao OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/kakao/callback. The override must stay on the web app origin at this exact path \u2014 the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.",
|
|
7364
7615
|
required: false,
|
|
7365
7616
|
examples: ["https://app.example.com/_auth/oauth/kakao/callback"]
|
|
7366
7617
|
})
|
|
@@ -7385,7 +7636,7 @@ var init_schema5 = __esm({
|
|
|
7385
7636
|
},
|
|
7386
7637
|
SPFN_AUTH_NAVER_REDIRECT_URI: {
|
|
7387
7638
|
...envString({
|
|
7388
|
-
description: "Naver OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/naver/callback.",
|
|
7639
|
+
description: "Naver OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/naver/callback. The override must stay on the web app origin at this exact path \u2014 the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.",
|
|
7389
7640
|
required: false,
|
|
7390
7641
|
examples: ["https://app.example.com/_auth/oauth/naver/callback"]
|
|
7391
7642
|
})
|
|
@@ -7417,11 +7668,18 @@ var init_schema5 = __esm({
|
|
|
7417
7668
|
},
|
|
7418
7669
|
SPFN_AUTH_GITHUB_REDIRECT_URI: {
|
|
7419
7670
|
...envString({
|
|
7420
|
-
description: "GitHub OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/github/callback.",
|
|
7671
|
+
description: "GitHub OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/github/callback. The override must stay on the web app origin at this exact path \u2014 the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.",
|
|
7421
7672
|
required: false,
|
|
7422
7673
|
examples: ["https://app.example.com/_auth/oauth/github/callback"]
|
|
7423
7674
|
})
|
|
7424
7675
|
},
|
|
7676
|
+
SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK: {
|
|
7677
|
+
...envString({
|
|
7678
|
+
description: 'Boot-time check of the four SPFN_AUTH_<PROVIDER>_REDIRECT_URI overrides: each one that is set must sit on the web app origin ({NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}) at /_auth/oauth/<provider>/callback, because the callback CSRF cookie is host-only. "off" is the only value that disables the check \u2014 unset, "on" and anything else all run it.',
|
|
7679
|
+
required: false,
|
|
7680
|
+
examples: ["off"]
|
|
7681
|
+
})
|
|
7682
|
+
},
|
|
7425
7683
|
// ============================================================================
|
|
7426
7684
|
// Native Social Login (mobile/web id_token verification)
|
|
7427
7685
|
//
|
|
@@ -7650,15 +7908,15 @@ var init_token_cipher = __esm({
|
|
|
7650
7908
|
});
|
|
7651
7909
|
|
|
7652
7910
|
// src/server/repositories/social-accounts.repository.ts
|
|
7653
|
-
import { eq as
|
|
7654
|
-
import { BaseRepository as
|
|
7911
|
+
import { eq as eq12, and as and9 } from "drizzle-orm";
|
|
7912
|
+
import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
|
|
7655
7913
|
var SocialAccountsRepository, socialAccountsRepository;
|
|
7656
7914
|
var init_social_accounts_repository = __esm({
|
|
7657
7915
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
7658
7916
|
"use strict";
|
|
7659
7917
|
init_entities();
|
|
7660
7918
|
init_token_cipher();
|
|
7661
|
-
SocialAccountsRepository = class extends
|
|
7919
|
+
SocialAccountsRepository = class extends BaseRepository12 {
|
|
7662
7920
|
/**
|
|
7663
7921
|
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
7664
7922
|
*
|
|
@@ -7686,10 +7944,10 @@ var init_social_accounts_repository = __esm({
|
|
|
7686
7944
|
if (refresh?.needsRotation) {
|
|
7687
7945
|
heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
|
|
7688
7946
|
}
|
|
7689
|
-
await this.db.update(userSocialAccounts).set(heal).where(
|
|
7690
|
-
|
|
7691
|
-
access?.needsRotation && account.accessToken !== null ?
|
|
7692
|
-
refresh?.needsRotation && account.refreshToken !== null ?
|
|
7947
|
+
await this.db.update(userSocialAccounts).set(heal).where(and9(
|
|
7948
|
+
eq12(userSocialAccounts.id, account.id),
|
|
7949
|
+
access?.needsRotation && account.accessToken !== null ? eq12(userSocialAccounts.accessToken, account.accessToken) : void 0,
|
|
7950
|
+
refresh?.needsRotation && account.refreshToken !== null ? eq12(userSocialAccounts.refreshToken, account.refreshToken) : void 0
|
|
7693
7951
|
));
|
|
7694
7952
|
} catch {
|
|
7695
7953
|
}
|
|
@@ -7706,9 +7964,9 @@ var init_social_accounts_repository = __esm({
|
|
|
7706
7964
|
*/
|
|
7707
7965
|
async findByProviderAndProviderId(provider, providerUserId) {
|
|
7708
7966
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7967
|
+
and9(
|
|
7968
|
+
eq12(userSocialAccounts.provider, provider),
|
|
7969
|
+
eq12(userSocialAccounts.providerUserId, providerUserId)
|
|
7712
7970
|
)
|
|
7713
7971
|
).limit(1);
|
|
7714
7972
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -7718,7 +7976,7 @@ var init_social_accounts_repository = __esm({
|
|
|
7718
7976
|
* Read replica 사용
|
|
7719
7977
|
*/
|
|
7720
7978
|
async findByUserId(userId) {
|
|
7721
|
-
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
7979
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq12(userSocialAccounts.userId, userId));
|
|
7722
7980
|
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
7723
7981
|
}
|
|
7724
7982
|
/**
|
|
@@ -7727,9 +7985,9 @@ var init_social_accounts_repository = __esm({
|
|
|
7727
7985
|
*/
|
|
7728
7986
|
async findByUserIdAndProvider(userId, provider) {
|
|
7729
7987
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7988
|
+
and9(
|
|
7989
|
+
eq12(userSocialAccounts.userId, userId),
|
|
7990
|
+
eq12(userSocialAccounts.provider, provider)
|
|
7733
7991
|
)
|
|
7734
7992
|
).limit(1);
|
|
7735
7993
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -7755,11 +8013,11 @@ var init_social_accounts_repository = __esm({
|
|
|
7755
8013
|
* 토큰 정보 업데이트
|
|
7756
8014
|
* Write primary 사용
|
|
7757
8015
|
*/
|
|
7758
|
-
async updateTokens(
|
|
8016
|
+
async updateTokens(id15, data) {
|
|
7759
8017
|
const accounts = await this.db.select({
|
|
7760
8018
|
provider: userSocialAccounts.provider,
|
|
7761
8019
|
providerUserId: userSocialAccounts.providerUserId
|
|
7762
|
-
}).from(userSocialAccounts).where(
|
|
8020
|
+
}).from(userSocialAccounts).where(eq12(userSocialAccounts.id, id15)).limit(1);
|
|
7763
8021
|
const account = accounts[0];
|
|
7764
8022
|
if (!account) {
|
|
7765
8023
|
return null;
|
|
@@ -7773,15 +8031,15 @@ var init_social_accounts_repository = __esm({
|
|
|
7773
8031
|
...data,
|
|
7774
8032
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
7775
8033
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
7776
|
-
}).where(
|
|
8034
|
+
}).where(eq12(userSocialAccounts.id, id15)).returning();
|
|
7777
8035
|
return this.decryptAccount(result[0] ?? null);
|
|
7778
8036
|
}
|
|
7779
8037
|
/**
|
|
7780
8038
|
* 소셜 계정 삭제
|
|
7781
8039
|
* Write primary 사용
|
|
7782
8040
|
*/
|
|
7783
|
-
async deleteById(
|
|
7784
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
8041
|
+
async deleteById(id15) {
|
|
8042
|
+
const result = await this.db.delete(userSocialAccounts).where(eq12(userSocialAccounts.id, id15)).returning();
|
|
7785
8043
|
return result[0] ?? null;
|
|
7786
8044
|
}
|
|
7787
8045
|
/**
|
|
@@ -7790,9 +8048,9 @@ var init_social_accounts_repository = __esm({
|
|
|
7790
8048
|
*/
|
|
7791
8049
|
async deleteByUserIdAndProvider(userId, provider) {
|
|
7792
8050
|
const result = await this.db.delete(userSocialAccounts).where(
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
8051
|
+
and9(
|
|
8052
|
+
eq12(userSocialAccounts.userId, userId),
|
|
8053
|
+
eq12(userSocialAccounts.provider, provider)
|
|
7796
8054
|
)
|
|
7797
8055
|
).returning();
|
|
7798
8056
|
return result[0] ?? null;
|
|
@@ -7805,7 +8063,7 @@ var init_social_accounts_repository = __esm({
|
|
|
7805
8063
|
* Write primary 사용
|
|
7806
8064
|
*/
|
|
7807
8065
|
async deleteAllByUserId(userId) {
|
|
7808
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
8066
|
+
const result = await this.db.delete(userSocialAccounts).where(eq12(userSocialAccounts.userId, userId)).returning();
|
|
7809
8067
|
return result.length;
|
|
7810
8068
|
}
|
|
7811
8069
|
};
|
|
@@ -7814,19 +8072,19 @@ var init_social_accounts_repository = __esm({
|
|
|
7814
8072
|
});
|
|
7815
8073
|
|
|
7816
8074
|
// src/server/repositories/auth-metadata.repository.ts
|
|
7817
|
-
import { BaseRepository as
|
|
7818
|
-
import { eq as
|
|
8075
|
+
import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
|
|
8076
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
7819
8077
|
var AuthMetadataRepository, authMetadataRepository;
|
|
7820
8078
|
var init_auth_metadata_repository = __esm({
|
|
7821
8079
|
"src/server/repositories/auth-metadata.repository.ts"() {
|
|
7822
8080
|
"use strict";
|
|
7823
8081
|
init_auth_metadata();
|
|
7824
|
-
AuthMetadataRepository = class extends
|
|
8082
|
+
AuthMetadataRepository = class extends BaseRepository13 {
|
|
7825
8083
|
/**
|
|
7826
8084
|
* 키로 값 조회
|
|
7827
8085
|
*/
|
|
7828
8086
|
async get(key) {
|
|
7829
|
-
const result = await this.readDb.select().from(authMetadata).where(
|
|
8087
|
+
const result = await this.readDb.select().from(authMetadata).where(eq13(authMetadata.key, key)).limit(1);
|
|
7830
8088
|
return result[0]?.value ?? null;
|
|
7831
8089
|
}
|
|
7832
8090
|
/**
|
|
@@ -7849,20 +8107,20 @@ var init_auth_metadata_repository = __esm({
|
|
|
7849
8107
|
});
|
|
7850
8108
|
|
|
7851
8109
|
// src/server/repositories/account-deletion-requests.repository.ts
|
|
7852
|
-
import { eq as
|
|
7853
|
-
import { BaseRepository as
|
|
8110
|
+
import { eq as eq14, and as and10, lte } from "drizzle-orm";
|
|
8111
|
+
import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
|
|
7854
8112
|
var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
|
|
7855
8113
|
var init_account_deletion_requests_repository = __esm({
|
|
7856
8114
|
"src/server/repositories/account-deletion-requests.repository.ts"() {
|
|
7857
8115
|
"use strict";
|
|
7858
8116
|
init_account_deletion_requests();
|
|
7859
|
-
AccountDeletionRequestsRepository = class extends
|
|
8117
|
+
AccountDeletionRequestsRepository = class extends BaseRepository14 {
|
|
7860
8118
|
/**
|
|
7861
8119
|
* ID로 요청 조회
|
|
7862
8120
|
* Read replica 사용
|
|
7863
8121
|
*/
|
|
7864
|
-
async findById(
|
|
7865
|
-
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
8122
|
+
async findById(id15) {
|
|
8123
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq14(accountDeletionRequests.id, id15)).limit(1);
|
|
7866
8124
|
return result[0] ?? null;
|
|
7867
8125
|
}
|
|
7868
8126
|
/**
|
|
@@ -7871,9 +8129,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7871
8129
|
*/
|
|
7872
8130
|
async findPendingByUserId(userId) {
|
|
7873
8131
|
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
8132
|
+
and10(
|
|
8133
|
+
eq14(accountDeletionRequests.userId, userId),
|
|
8134
|
+
eq14(accountDeletionRequests.status, "pending")
|
|
7877
8135
|
)
|
|
7878
8136
|
).limit(1);
|
|
7879
8137
|
return result[0] ?? null;
|
|
@@ -7887,9 +8145,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7887
8145
|
*/
|
|
7888
8146
|
async findPendingByUserIdOnPrimary(userId) {
|
|
7889
8147
|
const result = await this.db.select().from(accountDeletionRequests).where(
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
8148
|
+
and10(
|
|
8149
|
+
eq14(accountDeletionRequests.userId, userId),
|
|
8150
|
+
eq14(accountDeletionRequests.status, "pending")
|
|
7893
8151
|
)
|
|
7894
8152
|
).limit(1);
|
|
7895
8153
|
return result[0] ?? null;
|
|
@@ -7900,8 +8158,8 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7900
8158
|
*/
|
|
7901
8159
|
async findDueForPurge(now) {
|
|
7902
8160
|
return this.readDb.select().from(accountDeletionRequests).where(
|
|
7903
|
-
|
|
7904
|
-
|
|
8161
|
+
and10(
|
|
8162
|
+
eq14(accountDeletionRequests.status, "pending"),
|
|
7905
8163
|
lte(accountDeletionRequests.purgeScheduledAt, now)
|
|
7906
8164
|
)
|
|
7907
8165
|
);
|
|
@@ -7921,14 +8179,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7921
8179
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
7922
8180
|
* Write primary 사용
|
|
7923
8181
|
*/
|
|
7924
|
-
async markCancelled(
|
|
8182
|
+
async markCancelled(id15) {
|
|
7925
8183
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
7926
8184
|
status: "cancelled",
|
|
7927
8185
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
7928
8186
|
}).where(
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
8187
|
+
and10(
|
|
8188
|
+
eq14(accountDeletionRequests.id, id15),
|
|
8189
|
+
eq14(accountDeletionRequests.status, "pending")
|
|
7932
8190
|
)
|
|
7933
8191
|
).returning();
|
|
7934
8192
|
return result[0] ?? null;
|
|
@@ -7943,15 +8201,15 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7943
8201
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
7944
8202
|
* Write primary 사용
|
|
7945
8203
|
*/
|
|
7946
|
-
async markCompleted(
|
|
8204
|
+
async markCompleted(id15, purgeStrategy) {
|
|
7947
8205
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
7948
8206
|
status: "completed",
|
|
7949
8207
|
completedAt: /* @__PURE__ */ new Date(),
|
|
7950
8208
|
purgeStrategy
|
|
7951
8209
|
}).where(
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
8210
|
+
and10(
|
|
8211
|
+
eq14(accountDeletionRequests.id, id15),
|
|
8212
|
+
eq14(accountDeletionRequests.status, "pending")
|
|
7955
8213
|
)
|
|
7956
8214
|
).returning();
|
|
7957
8215
|
return result[0] ?? null;
|
|
@@ -7962,14 +8220,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7962
8220
|
});
|
|
7963
8221
|
|
|
7964
8222
|
// src/server/repositories/ops-tokens.repository.ts
|
|
7965
|
-
import { and as
|
|
7966
|
-
import { BaseRepository as
|
|
8223
|
+
import { and as and11, desc as desc3, eq as eq15, isNull as isNull5 } from "drizzle-orm";
|
|
8224
|
+
import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
|
|
7967
8225
|
var OpsTokensRepository, opsTokensRepository;
|
|
7968
8226
|
var init_ops_tokens_repository = __esm({
|
|
7969
8227
|
"src/server/repositories/ops-tokens.repository.ts"() {
|
|
7970
8228
|
"use strict";
|
|
7971
8229
|
init_ops_tokens();
|
|
7972
|
-
OpsTokensRepository = class extends
|
|
8230
|
+
OpsTokensRepository = class extends BaseRepository15 {
|
|
7973
8231
|
/**
|
|
7974
8232
|
* Lookup by the secret's hash — the verification path.
|
|
7975
8233
|
*
|
|
@@ -7979,7 +8237,7 @@ var init_ops_tokens_repository = __esm({
|
|
|
7979
8237
|
* and revocation is documented as taking effect immediately.
|
|
7980
8238
|
*/
|
|
7981
8239
|
async findByTokenHash(tokenHash) {
|
|
7982
|
-
const result = await this.db.select().from(opsTokens).where(
|
|
8240
|
+
const result = await this.db.select().from(opsTokens).where(eq15(opsTokens.tokenHash, tokenHash)).limit(1);
|
|
7983
8241
|
return result[0] ?? null;
|
|
7984
8242
|
}
|
|
7985
8243
|
async create(data) {
|
|
@@ -7994,13 +8252,13 @@ var init_ops_tokens_repository = __esm({
|
|
|
7994
8252
|
* token is already revoked — the first revocation's timestamp is never
|
|
7995
8253
|
* overwritten.
|
|
7996
8254
|
*/
|
|
7997
|
-
async revokeById(
|
|
7998
|
-
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
8255
|
+
async revokeById(id15) {
|
|
8256
|
+
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and11(eq15(opsTokens.id, id15), isNull5(opsTokens.revokedAt))).returning();
|
|
7999
8257
|
return result[0] ?? null;
|
|
8000
8258
|
}
|
|
8001
8259
|
/** Fire-and-forget from the verification path. */
|
|
8002
|
-
async updateLastUsedById(
|
|
8003
|
-
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
8260
|
+
async updateLastUsedById(id15) {
|
|
8261
|
+
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq15(opsTokens.id, id15));
|
|
8004
8262
|
}
|
|
8005
8263
|
};
|
|
8006
8264
|
opsTokensRepository = new OpsTokensRepository();
|
|
@@ -8015,6 +8273,7 @@ var init_repositories = __esm({
|
|
|
8015
8273
|
init_keys_repository();
|
|
8016
8274
|
init_verification_codes_repository();
|
|
8017
8275
|
init_signup_link_tokens_repository();
|
|
8276
|
+
init_device_authorizations_repository();
|
|
8018
8277
|
init_roles_repository();
|
|
8019
8278
|
init_permissions_repository();
|
|
8020
8279
|
init_role_permissions_repository();
|
|
@@ -8116,7 +8375,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
8116
8375
|
}
|
|
8117
8376
|
async function setRolePermissions(roleId, permissionIds) {
|
|
8118
8377
|
const roleIdNum = Number(roleId);
|
|
8119
|
-
const permissionIdNums = permissionIds.map((
|
|
8378
|
+
const permissionIdNums = permissionIds.map((id15) => Number(id15));
|
|
8120
8379
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
8121
8380
|
}
|
|
8122
8381
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -8136,7 +8395,7 @@ async function getRolePermissions(roleId) {
|
|
|
8136
8395
|
}
|
|
8137
8396
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
8138
8397
|
const perms = await Promise.all(
|
|
8139
|
-
permissionIds.map((
|
|
8398
|
+
permissionIds.map((id15) => permissionsRepository.findById(id15))
|
|
8140
8399
|
);
|
|
8141
8400
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
8142
8401
|
}
|
|
@@ -8222,6 +8481,7 @@ function validatePasswordStrength(password) {
|
|
|
8222
8481
|
import jwt from "jsonwebtoken";
|
|
8223
8482
|
import crypto2 from "crypto";
|
|
8224
8483
|
import { env as env2 } from "@spfn/auth/config";
|
|
8484
|
+
import { KeyAlgorithmMismatchError } from "@spfn/auth/errors";
|
|
8225
8485
|
var PUBLIC_KEY_CACHE_MAX = 1e3;
|
|
8226
8486
|
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
8227
8487
|
function getPublicKeyObject(publicKeyB64) {
|
|
@@ -8302,6 +8562,39 @@ function verifyKeyFingerprint(publicKeyB64, expectedFingerprint) {
|
|
|
8302
8562
|
return false;
|
|
8303
8563
|
}
|
|
8304
8564
|
}
|
|
8565
|
+
function readPublicKey(publicKeyB64) {
|
|
8566
|
+
try {
|
|
8567
|
+
return getPublicKeyObject(publicKeyB64);
|
|
8568
|
+
} catch {
|
|
8569
|
+
return null;
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
function describeKeyType(key) {
|
|
8573
|
+
if (!key) {
|
|
8574
|
+
return "not a readable SPKI public key";
|
|
8575
|
+
}
|
|
8576
|
+
if (key.asymmetricKeyType === "ec") {
|
|
8577
|
+
return `a ${key.asymmetricKeyDetails?.namedCurve ?? "unknown-curve"} EC key`;
|
|
8578
|
+
}
|
|
8579
|
+
return `a ${key.asymmetricKeyType ?? "unrecognised"} key`;
|
|
8580
|
+
}
|
|
8581
|
+
function keyMatchesAlgorithm(key, algorithm) {
|
|
8582
|
+
if (!key) {
|
|
8583
|
+
return false;
|
|
8584
|
+
}
|
|
8585
|
+
if (algorithm === "ES256") {
|
|
8586
|
+
return key.asymmetricKeyType === "ec" && key.asymmetricKeyDetails?.namedCurve === "prime256v1";
|
|
8587
|
+
}
|
|
8588
|
+
return key.asymmetricKeyType === "rsa";
|
|
8589
|
+
}
|
|
8590
|
+
function assertKeyMatchesAlgorithm(publicKeyB64, algorithm) {
|
|
8591
|
+
const key = readPublicKey(publicKeyB64);
|
|
8592
|
+
if (!keyMatchesAlgorithm(key, algorithm)) {
|
|
8593
|
+
throw new KeyAlgorithmMismatchError({
|
|
8594
|
+
message: `Public key is ${describeKeyType(key)} but the declared algorithm is ${algorithm}`
|
|
8595
|
+
});
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8305
8598
|
|
|
8306
8599
|
// src/server/helpers/context.ts
|
|
8307
8600
|
function getAuth(c) {
|
|
@@ -8353,6 +8646,27 @@ import {
|
|
|
8353
8646
|
// src/server/lib/config.ts
|
|
8354
8647
|
init_email();
|
|
8355
8648
|
import { env as env4 } from "@spfn/auth/config";
|
|
8649
|
+
|
|
8650
|
+
// src/server/logger.ts
|
|
8651
|
+
import { logger as rootLogger } from "@spfn/core/logger";
|
|
8652
|
+
var authLogger = {
|
|
8653
|
+
plugin: rootLogger.child("@spfn/auth:plugin"),
|
|
8654
|
+
middleware: rootLogger.child("@spfn/auth:middleware"),
|
|
8655
|
+
interceptor: {
|
|
8656
|
+
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
8657
|
+
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
8658
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
8659
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth"),
|
|
8660
|
+
csrf: rootLogger.child("@spfn/auth:interceptor:csrf")
|
|
8661
|
+
},
|
|
8662
|
+
session: rootLogger.child("@spfn/auth:session"),
|
|
8663
|
+
service: rootLogger.child("@spfn/auth:service"),
|
|
8664
|
+
setup: rootLogger.child("@spfn/auth:setup"),
|
|
8665
|
+
email: rootLogger.child("@spfn/auth:email"),
|
|
8666
|
+
sms: rootLogger.child("@spfn/auth:sms")
|
|
8667
|
+
};
|
|
8668
|
+
|
|
8669
|
+
// src/server/lib/config.ts
|
|
8356
8670
|
function getCookieSuffix() {
|
|
8357
8671
|
const port = process.env.SPFN_PORT;
|
|
8358
8672
|
return port ? `_${port}` : "";
|
|
@@ -8377,6 +8691,10 @@ var COOKIE_NAMES = {
|
|
|
8377
8691
|
/** Password-setup session for verified-email signup — temporary, single-purpose */
|
|
8378
8692
|
get SIGNUP_SETUP() {
|
|
8379
8693
|
return `spfn_signup_setup${getCookieSuffix()}`;
|
|
8694
|
+
},
|
|
8695
|
+
/** CSRF token — the only cookie here the browser can read */
|
|
8696
|
+
get CSRF() {
|
|
8697
|
+
return `spfn_csrf${getCookieSuffix()}`;
|
|
8380
8698
|
}
|
|
8381
8699
|
};
|
|
8382
8700
|
function matchOAuthCsrfCookies(cookies) {
|
|
@@ -8409,10 +8727,10 @@ var globalConfig = {
|
|
|
8409
8727
|
sessionTtl: "7d"
|
|
8410
8728
|
// Default: 7 days
|
|
8411
8729
|
};
|
|
8412
|
-
function configureAuth(
|
|
8730
|
+
function configureAuth(config3) {
|
|
8413
8731
|
globalConfig = {
|
|
8414
8732
|
...globalConfig,
|
|
8415
|
-
...
|
|
8733
|
+
...config3
|
|
8416
8734
|
};
|
|
8417
8735
|
}
|
|
8418
8736
|
function getAuthConfig() {
|
|
@@ -8437,6 +8755,28 @@ function getSessionTtl(override) {
|
|
|
8437
8755
|
}
|
|
8438
8756
|
return 7 * 24 * 60 * 60;
|
|
8439
8757
|
}
|
|
8758
|
+
var CSRF_MODES = ["off", "warn", "enforce"];
|
|
8759
|
+
var unrecognizedCsrfModeReported = false;
|
|
8760
|
+
function getCsrfMode() {
|
|
8761
|
+
const configured2 = globalConfig.csrf?.mode ?? env4.SPFN_AUTH_CSRF;
|
|
8762
|
+
if (!configured2) {
|
|
8763
|
+
return "warn";
|
|
8764
|
+
}
|
|
8765
|
+
const normalized = String(configured2).trim().toLowerCase();
|
|
8766
|
+
if (!CSRF_MODES.includes(normalized)) {
|
|
8767
|
+
if (!unrecognizedCsrfModeReported) {
|
|
8768
|
+
unrecognizedCsrfModeReported = true;
|
|
8769
|
+
authLogger.interceptor.csrf.error(
|
|
8770
|
+
`Unrecognized CSRF mode "${configured2}" \u2014 expected off | warn | enforce. Enforcing.`
|
|
8771
|
+
);
|
|
8772
|
+
}
|
|
8773
|
+
return "enforce";
|
|
8774
|
+
}
|
|
8775
|
+
return normalized;
|
|
8776
|
+
}
|
|
8777
|
+
function getCsrfExemptPaths() {
|
|
8778
|
+
return globalConfig.csrf?.exemptPaths ?? [];
|
|
8779
|
+
}
|
|
8440
8780
|
|
|
8441
8781
|
// src/server/services/verification.service.ts
|
|
8442
8782
|
import crypto4 from "crypto";
|
|
@@ -8444,26 +8784,6 @@ import { env as env5 } from "@spfn/auth/config";
|
|
|
8444
8784
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
8445
8785
|
import jwt2 from "jsonwebtoken";
|
|
8446
8786
|
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
8447
|
-
|
|
8448
|
-
// src/server/logger.ts
|
|
8449
|
-
import { logger as rootLogger } from "@spfn/core/logger";
|
|
8450
|
-
var authLogger = {
|
|
8451
|
-
plugin: rootLogger.child("@spfn/auth:plugin"),
|
|
8452
|
-
middleware: rootLogger.child("@spfn/auth:middleware"),
|
|
8453
|
-
interceptor: {
|
|
8454
|
-
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
8455
|
-
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
8456
|
-
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
8457
|
-
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
8458
|
-
},
|
|
8459
|
-
session: rootLogger.child("@spfn/auth:session"),
|
|
8460
|
-
service: rootLogger.child("@spfn/auth:service"),
|
|
8461
|
-
setup: rootLogger.child("@spfn/auth:setup"),
|
|
8462
|
-
email: rootLogger.child("@spfn/auth:email"),
|
|
8463
|
-
sms: rootLogger.child("@spfn/auth:sms")
|
|
8464
|
-
};
|
|
8465
|
-
|
|
8466
|
-
// src/server/services/verification.service.ts
|
|
8467
8787
|
init_email();
|
|
8468
8788
|
init_repositories();
|
|
8469
8789
|
var ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES = 60;
|
|
@@ -8646,6 +8966,7 @@ var KEY_TTL_DAYS = 90;
|
|
|
8646
8966
|
init_repositories();
|
|
8647
8967
|
import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/auth/errors";
|
|
8648
8968
|
var KEY_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
8969
|
+
var DEFAULT_KEY_ALGORITHM = "ES256";
|
|
8649
8970
|
function getKeyExpiryDate() {
|
|
8650
8971
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
8651
8972
|
expiresAt.setDate(expiresAt.getDate() + KEY_TTL_DAYS);
|
|
@@ -8655,7 +8976,7 @@ function isExpired(expiresAt) {
|
|
|
8655
8976
|
return expiresAt !== null && /* @__PURE__ */ new Date() > expiresAt;
|
|
8656
8977
|
}
|
|
8657
8978
|
async function registerPublicKeyService(params) {
|
|
8658
|
-
const { userId, keyId, publicKey, fingerprint, algorithm =
|
|
8979
|
+
const { userId, keyId, publicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM, deviceName, platform } = params;
|
|
8659
8980
|
const existing = await keysRepository.findByKeyId(keyId);
|
|
8660
8981
|
if (existing) {
|
|
8661
8982
|
if (existing.userId === userId && existing.isActive) {
|
|
@@ -8670,6 +8991,7 @@ async function registerPublicKeyService(params) {
|
|
|
8670
8991
|
if (!isValidFingerprint) {
|
|
8671
8992
|
throw new InvalidKeyFingerprintError();
|
|
8672
8993
|
}
|
|
8994
|
+
assertKeyMatchesAlgorithm(publicKey, algorithm);
|
|
8673
8995
|
await keysRepository.create({
|
|
8674
8996
|
userId,
|
|
8675
8997
|
keyId,
|
|
@@ -8683,11 +9005,12 @@ async function registerPublicKeyService(params) {
|
|
|
8683
9005
|
});
|
|
8684
9006
|
}
|
|
8685
9007
|
async function rotateKeyService(params) {
|
|
8686
|
-
const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm =
|
|
9008
|
+
const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM } = params;
|
|
8687
9009
|
const isValidFingerprint = verifyKeyFingerprint(newPublicKey, fingerprint);
|
|
8688
9010
|
if (!isValidFingerprint) {
|
|
8689
9011
|
throw new InvalidKeyFingerprintError();
|
|
8690
9012
|
}
|
|
9013
|
+
assertKeyMatchesAlgorithm(newPublicKey, algorithm);
|
|
8691
9014
|
const replaced = await keysRepository.findByKeyIdAndUserId(oldKeyId, userId);
|
|
8692
9015
|
await keysRepository.revokeByKeyIdAndUserId(
|
|
8693
9016
|
oldKeyId,
|
|
@@ -8734,6 +9057,7 @@ async function listKeysService(params) {
|
|
|
8734
9057
|
async function revokeAllKeysService(params) {
|
|
8735
9058
|
const { userId, currentKeyId, includeCurrent = false, reason } = params;
|
|
8736
9059
|
const revoked = includeCurrent ? await keysRepository.revokeAllActiveByUserId(userId, reason) : await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason);
|
|
9060
|
+
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
8737
9061
|
return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
|
|
8738
9062
|
}
|
|
8739
9063
|
|
|
@@ -8859,11 +9183,17 @@ var AuthProviderSchema = Type.Union([
|
|
|
8859
9183
|
Type.Literal("phone"),
|
|
8860
9184
|
...SOCIAL_PROVIDERS.map((p) => Type.Literal(p))
|
|
8861
9185
|
]);
|
|
9186
|
+
var AuthLoginProviderSchema = Type.Union([
|
|
9187
|
+
Type.Literal("email"),
|
|
9188
|
+
Type.Literal("phone"),
|
|
9189
|
+
Type.Literal("device"),
|
|
9190
|
+
...SOCIAL_PROVIDERS.map((p) => Type.Literal(p))
|
|
9191
|
+
]);
|
|
8862
9192
|
var authLoginEvent = defineEvent(
|
|
8863
9193
|
"auth.login",
|
|
8864
9194
|
Type.Object({
|
|
8865
9195
|
userId: Type.String(),
|
|
8866
|
-
provider:
|
|
9196
|
+
provider: AuthLoginProviderSchema,
|
|
8867
9197
|
email: Type.Optional(Type.String()),
|
|
8868
9198
|
phone: Type.Optional(Type.String())
|
|
8869
9199
|
})
|
|
@@ -8971,8 +9301,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
8971
9301
|
throw new VerificationTokenTargetMismatchError();
|
|
8972
9302
|
}
|
|
8973
9303
|
}
|
|
8974
|
-
async function sendDeletionEmail(to, subject,
|
|
8975
|
-
const result = await sendEmail2({ to, subject, text:
|
|
9304
|
+
async function sendDeletionEmail(to, subject, text15) {
|
|
9305
|
+
const result = await sendEmail2({ to, subject, text: text15 });
|
|
8976
9306
|
if (!result.success) {
|
|
8977
9307
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
8978
9308
|
}
|
|
@@ -9024,12 +9354,12 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
9024
9354
|
if (requestedBy === "self") {
|
|
9025
9355
|
await verifyReauthCredential(user, { password, verificationToken });
|
|
9026
9356
|
}
|
|
9027
|
-
const
|
|
9357
|
+
const config3 = getDeletionConfig();
|
|
9028
9358
|
const wantsImmediate = immediate === true;
|
|
9029
|
-
if (wantsImmediate && requestedBy === "self" && !
|
|
9359
|
+
if (wantsImmediate && requestedBy === "self" && !config3.allowSelfImmediate) {
|
|
9030
9360
|
throw new ImmediateDeletionNotAllowedError();
|
|
9031
9361
|
}
|
|
9032
|
-
const gracePeriodDays = wantsImmediate ? 0 :
|
|
9362
|
+
const gracePeriodDays = wantsImmediate ? 0 : config3.gracePeriodDays;
|
|
9033
9363
|
const requestedAt = /* @__PURE__ */ new Date();
|
|
9034
9364
|
const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
|
|
9035
9365
|
await usersRepository.updateById(user.id, { status: "pending_deletion" });
|
|
@@ -9051,6 +9381,7 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
9051
9381
|
throw error;
|
|
9052
9382
|
}
|
|
9053
9383
|
await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
|
|
9384
|
+
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
9054
9385
|
onAfterCommit(() => authDeletionRequestedEvent.emit({
|
|
9055
9386
|
userId: String(user.id),
|
|
9056
9387
|
userPublicId: user.publicId,
|
|
@@ -9133,10 +9464,10 @@ async function purgePendingRequest(request) {
|
|
|
9133
9464
|
if (!precheckUser || precheckUser.status !== "pending_deletion") {
|
|
9134
9465
|
return { outcome: "skipped" };
|
|
9135
9466
|
}
|
|
9136
|
-
const
|
|
9137
|
-
if (
|
|
9467
|
+
const config3 = getDeletionConfig();
|
|
9468
|
+
if (config3.onBeforePurge) {
|
|
9138
9469
|
try {
|
|
9139
|
-
await
|
|
9470
|
+
await config3.onBeforePurge({
|
|
9140
9471
|
id: precheckUser.id,
|
|
9141
9472
|
publicId: precheckUser.publicId,
|
|
9142
9473
|
email: precheckUser.email,
|
|
@@ -9150,7 +9481,7 @@ async function purgePendingRequest(request) {
|
|
|
9150
9481
|
return { outcome: "skipped" };
|
|
9151
9482
|
}
|
|
9152
9483
|
}
|
|
9153
|
-
const purgeStrategy =
|
|
9484
|
+
const purgeStrategy = config3.purgeStrategy;
|
|
9154
9485
|
let purgedUser = null;
|
|
9155
9486
|
await runInTransaction(async () => {
|
|
9156
9487
|
const user = await usersRepository.findById(userId);
|
|
@@ -9371,6 +9702,7 @@ async function changePasswordService(params) {
|
|
|
9371
9702
|
}
|
|
9372
9703
|
const newPasswordHash = await hashPassword(newPassword);
|
|
9373
9704
|
await usersRepository.updatePassword(userId, newPasswordHash, true);
|
|
9705
|
+
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
9374
9706
|
await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
|
|
9375
9707
|
}
|
|
9376
9708
|
|
|
@@ -9513,23 +9845,252 @@ async function completeSignupService(params) {
|
|
|
9513
9845
|
authLogger.service.warn("Signup setup session refused", { reason: "lost the claim race" });
|
|
9514
9846
|
throw new InvalidSignupSetupSessionError();
|
|
9515
9847
|
}
|
|
9516
|
-
return await createVerifiedAccount({
|
|
9517
|
-
email: claimed.email,
|
|
9518
|
-
password: params.password,
|
|
9519
|
-
publicKey: params.publicKey,
|
|
9520
|
-
keyId: params.keyId,
|
|
9521
|
-
fingerprint: params.fingerprint,
|
|
9522
|
-
algorithm: params.algorithm,
|
|
9523
|
-
deviceName: params.deviceName,
|
|
9524
|
-
platform: params.platform,
|
|
9525
|
-
metadata: params.metadata
|
|
9848
|
+
return await createVerifiedAccount({
|
|
9849
|
+
email: claimed.email,
|
|
9850
|
+
password: params.password,
|
|
9851
|
+
publicKey: params.publicKey,
|
|
9852
|
+
keyId: params.keyId,
|
|
9853
|
+
fingerprint: params.fingerprint,
|
|
9854
|
+
algorithm: params.algorithm,
|
|
9855
|
+
deviceName: params.deviceName,
|
|
9856
|
+
platform: params.platform,
|
|
9857
|
+
metadata: params.metadata
|
|
9858
|
+
});
|
|
9859
|
+
}
|
|
9860
|
+
|
|
9861
|
+
// src/server/services/device-auth.service.ts
|
|
9862
|
+
init_repositories();
|
|
9863
|
+
import {
|
|
9864
|
+
AccountDisabledError as AccountDisabledError2,
|
|
9865
|
+
AccountPendingDeletionError as AccountPendingDeletionError2,
|
|
9866
|
+
DeviceAuthNotFoundError,
|
|
9867
|
+
DeviceAuthExpiredError,
|
|
9868
|
+
DeviceAuthAlreadyHandledError,
|
|
9869
|
+
DeviceAuthDeniedError,
|
|
9870
|
+
InvalidKeyFingerprintError as InvalidKeyFingerprintError2
|
|
9871
|
+
} from "@spfn/auth/errors";
|
|
9872
|
+
import { onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
|
|
9873
|
+
|
|
9874
|
+
// src/server/lib/device-auth-config.ts
|
|
9875
|
+
var DEFAULT_DEVICE_AUTH_TTL_MS = 10 * 60 * 1e3;
|
|
9876
|
+
var DEFAULT_DEVICE_AUTH_INTERVAL_MS = 5 * 1e3;
|
|
9877
|
+
var config2 = {
|
|
9878
|
+
ttlMs: DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
9879
|
+
intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS
|
|
9880
|
+
};
|
|
9881
|
+
function configureDeviceAuth(options) {
|
|
9882
|
+
const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_AUTH_TTL_MS;
|
|
9883
|
+
const intervalMs = options?.intervalMs ?? DEFAULT_DEVICE_AUTH_INTERVAL_MS;
|
|
9884
|
+
assertWholeMillis("ttlMs", ttlMs);
|
|
9885
|
+
assertWholeMillis("intervalMs", intervalMs);
|
|
9886
|
+
config2 = { ttlMs, intervalMs };
|
|
9887
|
+
}
|
|
9888
|
+
function assertWholeMillis(name, value) {
|
|
9889
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
9890
|
+
throw new Error(
|
|
9891
|
+
`deviceAuth.${name} must be a positive whole number of milliseconds, received ${value}.`
|
|
9892
|
+
);
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
9895
|
+
function getDeviceAuthConfig() {
|
|
9896
|
+
return config2;
|
|
9897
|
+
}
|
|
9898
|
+
|
|
9899
|
+
// src/server/lib/device-code.ts
|
|
9900
|
+
import { createHash, randomBytes, randomInt } from "crypto";
|
|
9901
|
+
var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
9902
|
+
var USER_CODE_LENGTH = 8;
|
|
9903
|
+
var USER_CODE_GROUP_SIZE = 4;
|
|
9904
|
+
var DEVICE_CODE_BYTES = 32;
|
|
9905
|
+
function generateUserCode() {
|
|
9906
|
+
let code = "";
|
|
9907
|
+
for (let position = 0; position < USER_CODE_LENGTH; position++) {
|
|
9908
|
+
code += USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)];
|
|
9909
|
+
}
|
|
9910
|
+
return code;
|
|
9911
|
+
}
|
|
9912
|
+
function formatUserCode(userCode) {
|
|
9913
|
+
return `${userCode.slice(0, USER_CODE_GROUP_SIZE)}-${userCode.slice(USER_CODE_GROUP_SIZE)}`;
|
|
9914
|
+
}
|
|
9915
|
+
function normalizeUserCode(input) {
|
|
9916
|
+
return input.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
9917
|
+
}
|
|
9918
|
+
function generateDeviceCode() {
|
|
9919
|
+
return randomBytes(DEVICE_CODE_BYTES).toString("base64url");
|
|
9920
|
+
}
|
|
9921
|
+
function hashDeviceCode(deviceCode) {
|
|
9922
|
+
return createHash("sha256").update(deviceCode).digest("hex");
|
|
9923
|
+
}
|
|
9924
|
+
|
|
9925
|
+
// src/server/services/device-auth.service.ts
|
|
9926
|
+
var USER_CODE_ATTEMPTS = 3;
|
|
9927
|
+
function assertActionable(record) {
|
|
9928
|
+
if (!record || record.status === "consumed") {
|
|
9929
|
+
throw new DeviceAuthNotFoundError();
|
|
9930
|
+
}
|
|
9931
|
+
if (record.expiresAt.getTime() < Date.now()) {
|
|
9932
|
+
throw new DeviceAuthExpiredError();
|
|
9933
|
+
}
|
|
9934
|
+
return record;
|
|
9935
|
+
}
|
|
9936
|
+
function refuseMissedTransition(record, from, moved) {
|
|
9937
|
+
if (!record || record.status === "consumed") {
|
|
9938
|
+
throw new DeviceAuthNotFoundError();
|
|
9939
|
+
}
|
|
9940
|
+
if (record.status === from) {
|
|
9941
|
+
throw new DeviceAuthExpiredError();
|
|
9942
|
+
}
|
|
9943
|
+
throw moved();
|
|
9944
|
+
}
|
|
9945
|
+
function describeDevice(record) {
|
|
9946
|
+
return {
|
|
9947
|
+
deviceName: record.deviceName ?? void 0,
|
|
9948
|
+
platform: record.platform ?? void 0,
|
|
9949
|
+
fingerprintPrefix: record.fingerprint.slice(0, KEY_FINGERPRINT_PREFIX_LENGTH),
|
|
9950
|
+
requestedAtMillis: record.createdAt.getTime(),
|
|
9951
|
+
expiresAtMillis: record.expiresAt.getTime()
|
|
9952
|
+
};
|
|
9953
|
+
}
|
|
9954
|
+
async function startDeviceAuthService(params) {
|
|
9955
|
+
if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
|
|
9956
|
+
throw new InvalidKeyFingerprintError2();
|
|
9957
|
+
}
|
|
9958
|
+
assertKeyMatchesAlgorithm(params.publicKey, params.algorithm ?? DEFAULT_KEY_ALGORITHM);
|
|
9959
|
+
const { ttlMs, intervalMs } = getDeviceAuthConfig();
|
|
9960
|
+
const expiresAt = new Date(Date.now() + ttlMs);
|
|
9961
|
+
for (let attempt = 0; attempt < USER_CODE_ATTEMPTS; attempt++) {
|
|
9962
|
+
const deviceCode = generateDeviceCode();
|
|
9963
|
+
const userCode = generateUserCode();
|
|
9964
|
+
const record = await deviceAuthorizationsRepository.create({
|
|
9965
|
+
deviceCodeHash: hashDeviceCode(deviceCode),
|
|
9966
|
+
userCode,
|
|
9967
|
+
publicKey: params.publicKey,
|
|
9968
|
+
keyId: params.keyId,
|
|
9969
|
+
fingerprint: params.fingerprint,
|
|
9970
|
+
algorithm: params.algorithm,
|
|
9971
|
+
deviceName: params.deviceName,
|
|
9972
|
+
platform: params.platform,
|
|
9973
|
+
expiresAt
|
|
9974
|
+
});
|
|
9975
|
+
if (record) {
|
|
9976
|
+
return {
|
|
9977
|
+
deviceCode,
|
|
9978
|
+
userCode: formatUserCode(userCode),
|
|
9979
|
+
expiresAtMillis: expiresAt.getTime(),
|
|
9980
|
+
intervalMillis: intervalMs
|
|
9981
|
+
};
|
|
9982
|
+
}
|
|
9983
|
+
}
|
|
9984
|
+
throw new Error(
|
|
9985
|
+
`Could not allocate a unique device user code in ${USER_CODE_ATTEMPTS} attempts. Check the code generator and the user_code unique index.`
|
|
9986
|
+
);
|
|
9987
|
+
}
|
|
9988
|
+
async function getDeviceAuthInfoService(params) {
|
|
9989
|
+
const record = assertActionable(
|
|
9990
|
+
await deviceAuthorizationsRepository.findByUserCode(normalizeUserCode(params.userCode))
|
|
9991
|
+
);
|
|
9992
|
+
if (record.status !== "pending") {
|
|
9993
|
+
throw new DeviceAuthAlreadyHandledError();
|
|
9994
|
+
}
|
|
9995
|
+
return describeDevice(record);
|
|
9996
|
+
}
|
|
9997
|
+
async function approveDeviceAuthService(params) {
|
|
9998
|
+
const userCode = normalizeUserCode(params.userCode);
|
|
9999
|
+
const record = assertActionable(
|
|
10000
|
+
await deviceAuthorizationsRepository.findByUserCode(userCode)
|
|
10001
|
+
);
|
|
10002
|
+
const approved = await deviceAuthorizationsRepository.approve(record.id, params.userId);
|
|
10003
|
+
if (!approved) {
|
|
10004
|
+
refuseMissedTransition(
|
|
10005
|
+
await deviceAuthorizationsRepository.findByUserCode(userCode),
|
|
10006
|
+
"pending",
|
|
10007
|
+
() => new DeviceAuthAlreadyHandledError()
|
|
10008
|
+
);
|
|
10009
|
+
}
|
|
10010
|
+
return describeDevice(approved);
|
|
10011
|
+
}
|
|
10012
|
+
async function denyDeviceAuthService(params) {
|
|
10013
|
+
const userCode = normalizeUserCode(params.userCode);
|
|
10014
|
+
const record = assertActionable(
|
|
10015
|
+
await deviceAuthorizationsRepository.findByUserCode(userCode)
|
|
10016
|
+
);
|
|
10017
|
+
const denied = await deviceAuthorizationsRepository.deny(record.id);
|
|
10018
|
+
if (!denied) {
|
|
10019
|
+
refuseMissedTransition(
|
|
10020
|
+
await deviceAuthorizationsRepository.findByUserCode(userCode),
|
|
10021
|
+
"pending",
|
|
10022
|
+
() => new DeviceAuthAlreadyHandledError()
|
|
10023
|
+
);
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
async function pollDeviceAuthService(params) {
|
|
10027
|
+
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
10028
|
+
const record = assertActionable(
|
|
10029
|
+
await deviceAuthorizationsRepository.findByDeviceCodeHash(deviceCodeHash)
|
|
10030
|
+
);
|
|
10031
|
+
if (record.status === "denied") {
|
|
10032
|
+
throw new DeviceAuthDeniedError();
|
|
10033
|
+
}
|
|
10034
|
+
if (record.status === "pending") {
|
|
10035
|
+
return { status: "pending", intervalMillis: getDeviceAuthConfig().intervalMs };
|
|
10036
|
+
}
|
|
10037
|
+
const consumed = await deviceAuthorizationsRepository.consumeApproved(deviceCodeHash);
|
|
10038
|
+
if (!consumed) {
|
|
10039
|
+
refuseMissedTransition(
|
|
10040
|
+
await deviceAuthorizationsRepository.findByDeviceCodeHash(deviceCodeHash),
|
|
10041
|
+
"approved",
|
|
10042
|
+
() => new DeviceAuthNotFoundError()
|
|
10043
|
+
);
|
|
10044
|
+
}
|
|
10045
|
+
return { status: "approved", ...await completeDeviceLogin(consumed) };
|
|
10046
|
+
}
|
|
10047
|
+
async function completeDeviceLogin(record) {
|
|
10048
|
+
if (record.userId === null) {
|
|
10049
|
+
throw new DeviceAuthNotFoundError();
|
|
10050
|
+
}
|
|
10051
|
+
const user = await usersRepository.findById(record.userId);
|
|
10052
|
+
if (!user) {
|
|
10053
|
+
throw new DeviceAuthNotFoundError();
|
|
10054
|
+
}
|
|
10055
|
+
if (user.status !== "active") {
|
|
10056
|
+
if (user.status === "pending_deletion") {
|
|
10057
|
+
const pending = await getPendingDeletionInfo(user.id);
|
|
10058
|
+
throw new AccountPendingDeletionError2({
|
|
10059
|
+
purgeScheduledAt: pending?.purgeScheduledAt.toISOString()
|
|
10060
|
+
});
|
|
10061
|
+
}
|
|
10062
|
+
throw new AccountDisabledError2({ status: user.status });
|
|
10063
|
+
}
|
|
10064
|
+
await registerPublicKeyService({
|
|
10065
|
+
userId: user.id,
|
|
10066
|
+
keyId: record.keyId,
|
|
10067
|
+
publicKey: record.publicKey,
|
|
10068
|
+
fingerprint: record.fingerprint,
|
|
10069
|
+
algorithm: record.algorithm,
|
|
10070
|
+
deviceName: record.deviceName ?? void 0,
|
|
10071
|
+
platform: record.platform ?? void 0
|
|
9526
10072
|
});
|
|
10073
|
+
await updateLastLoginService(user.id);
|
|
10074
|
+
const result = {
|
|
10075
|
+
userId: String(user.id),
|
|
10076
|
+
publicId: user.publicId,
|
|
10077
|
+
email: user.email || void 0,
|
|
10078
|
+
phone: user.phone || void 0,
|
|
10079
|
+
passwordChangeRequired: user.passwordChangeRequired
|
|
10080
|
+
};
|
|
10081
|
+
onAfterCommit2(() => authLoginEvent.emit({
|
|
10082
|
+
userId: result.userId,
|
|
10083
|
+
provider: "device",
|
|
10084
|
+
email: result.email,
|
|
10085
|
+
phone: result.phone
|
|
10086
|
+
}));
|
|
10087
|
+
return result;
|
|
9527
10088
|
}
|
|
9528
10089
|
|
|
9529
10090
|
// src/server/services/rbac.service.ts
|
|
9530
10091
|
init_repositories();
|
|
9531
10092
|
init_rbac();
|
|
9532
|
-
import { createHash } from "crypto";
|
|
10093
|
+
import { createHash as createHash2 } from "crypto";
|
|
9533
10094
|
var RBAC_HASH_KEY = "rbac_config_hash";
|
|
9534
10095
|
function computeConfigHash(allRoles, allPermissions, allMappings) {
|
|
9535
10096
|
const payload = JSON.stringify({
|
|
@@ -9540,7 +10101,7 @@ function computeConfigHash(allRoles, allPermissions, allMappings) {
|
|
|
9540
10101
|
return acc;
|
|
9541
10102
|
}, {})
|
|
9542
10103
|
});
|
|
9543
|
-
return
|
|
10104
|
+
return createHash2("sha256").update(payload).digest("hex");
|
|
9544
10105
|
}
|
|
9545
10106
|
function collectMappings(options) {
|
|
9546
10107
|
const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
|
|
@@ -9598,51 +10159,51 @@ async function initializeAuth(options = {}) {
|
|
|
9598
10159
|
authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
|
|
9599
10160
|
}
|
|
9600
10161
|
async function syncRoles(configs, existingByName) {
|
|
9601
|
-
for (const
|
|
9602
|
-
const existing = existingByName.get(
|
|
10162
|
+
for (const config3 of configs) {
|
|
10163
|
+
const existing = existingByName.get(config3.name);
|
|
9603
10164
|
if (!existing) {
|
|
9604
10165
|
await rolesRepository.create({
|
|
9605
|
-
name:
|
|
9606
|
-
displayName:
|
|
9607
|
-
description:
|
|
9608
|
-
priority:
|
|
9609
|
-
isSystem:
|
|
9610
|
-
isBuiltin:
|
|
10166
|
+
name: config3.name,
|
|
10167
|
+
displayName: config3.displayName,
|
|
10168
|
+
description: config3.description || null,
|
|
10169
|
+
priority: config3.priority ?? 10,
|
|
10170
|
+
isSystem: config3.isSystem ?? false,
|
|
10171
|
+
isBuiltin: config3.isBuiltin ?? false,
|
|
9611
10172
|
isActive: true
|
|
9612
10173
|
});
|
|
9613
|
-
authLogger.service.info(` \u2705 Created role: ${
|
|
10174
|
+
authLogger.service.info(` \u2705 Created role: ${config3.name}`);
|
|
9614
10175
|
} else {
|
|
9615
10176
|
const updateData = {
|
|
9616
|
-
displayName:
|
|
9617
|
-
description:
|
|
10177
|
+
displayName: config3.displayName,
|
|
10178
|
+
description: config3.description || null
|
|
9618
10179
|
};
|
|
9619
10180
|
if (!existing.isBuiltin) {
|
|
9620
|
-
updateData.priority =
|
|
10181
|
+
updateData.priority = config3.priority ?? existing.priority;
|
|
9621
10182
|
}
|
|
9622
10183
|
await rolesRepository.updateById(existing.id, updateData);
|
|
9623
10184
|
}
|
|
9624
10185
|
}
|
|
9625
10186
|
}
|
|
9626
10187
|
async function syncPermissions(configs, existingByName) {
|
|
9627
|
-
for (const
|
|
9628
|
-
const existing = existingByName.get(
|
|
10188
|
+
for (const config3 of configs) {
|
|
10189
|
+
const existing = existingByName.get(config3.name);
|
|
9629
10190
|
if (!existing) {
|
|
9630
10191
|
await permissionsRepository.create({
|
|
9631
|
-
name:
|
|
9632
|
-
displayName:
|
|
9633
|
-
description:
|
|
9634
|
-
category:
|
|
9635
|
-
isSystem:
|
|
9636
|
-
isBuiltin:
|
|
10192
|
+
name: config3.name,
|
|
10193
|
+
displayName: config3.displayName,
|
|
10194
|
+
description: config3.description || null,
|
|
10195
|
+
category: config3.category || null,
|
|
10196
|
+
isSystem: config3.isSystem ?? false,
|
|
10197
|
+
isBuiltin: config3.isBuiltin ?? false,
|
|
9637
10198
|
isActive: true,
|
|
9638
10199
|
metadata: null
|
|
9639
10200
|
});
|
|
9640
|
-
authLogger.service.info(` \u2705 Created permission: ${
|
|
10201
|
+
authLogger.service.info(` \u2705 Created permission: ${config3.name}`);
|
|
9641
10202
|
} else {
|
|
9642
10203
|
await permissionsRepository.updateById(existing.id, {
|
|
9643
|
-
displayName:
|
|
9644
|
-
description:
|
|
9645
|
-
category:
|
|
10204
|
+
displayName: config3.displayName,
|
|
10205
|
+
description: config3.description || null,
|
|
10206
|
+
category: config3.category || null
|
|
9646
10207
|
});
|
|
9647
10208
|
}
|
|
9648
10209
|
}
|
|
@@ -9703,7 +10264,7 @@ async function getUserPermissions(userId) {
|
|
|
9703
10264
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
9704
10265
|
if (permIds.length > 0) {
|
|
9705
10266
|
const rolePerms = await Promise.all(
|
|
9706
|
-
permIds.map((
|
|
10267
|
+
permIds.map((id15) => permissionsRepository.findById(id15))
|
|
9707
10268
|
);
|
|
9708
10269
|
for (const perm of rolePerms) {
|
|
9709
10270
|
if (perm && perm.isActive) {
|
|
@@ -9860,6 +10421,7 @@ async function validateInvitation(token) {
|
|
|
9860
10421
|
}
|
|
9861
10422
|
async function acceptInvitation(params) {
|
|
9862
10423
|
const { token, password, publicKey, keyId, fingerprint, algorithm } = params;
|
|
10424
|
+
assertKeyMatchesAlgorithm(publicKey, algorithm);
|
|
9863
10425
|
const validation = await validateInvitation(token);
|
|
9864
10426
|
if (!validation.valid || !validation.invitation) {
|
|
9865
10427
|
throw new BadRequestError({ message: validation.error || "Invalid invitation" });
|
|
@@ -9916,20 +10478,20 @@ async function acceptInvitation(params) {
|
|
|
9916
10478
|
async function listInvitations(params) {
|
|
9917
10479
|
return await invitationsRepository.list(params);
|
|
9918
10480
|
}
|
|
9919
|
-
async function cancelInvitation(
|
|
9920
|
-
const invitation = await invitationsRepository.findById(
|
|
10481
|
+
async function cancelInvitation(id15, cancelledBy, reason) {
|
|
10482
|
+
const invitation = await invitationsRepository.findById(id15);
|
|
9921
10483
|
if (!invitation) {
|
|
9922
10484
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
9923
10485
|
}
|
|
9924
10486
|
if (invitation.status !== "pending") {
|
|
9925
10487
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
9926
10488
|
}
|
|
9927
|
-
await invitationsRepository.cancel(
|
|
10489
|
+
await invitationsRepository.cancel(id15, cancelledBy, reason, invitation.metadata);
|
|
9928
10490
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
9929
10491
|
}
|
|
9930
|
-
async function deleteInvitation(
|
|
9931
|
-
await invitationsRepository.deleteById(
|
|
9932
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
10492
|
+
async function deleteInvitation(id15) {
|
|
10493
|
+
await invitationsRepository.deleteById(id15);
|
|
10494
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id15}`);
|
|
9933
10495
|
}
|
|
9934
10496
|
async function expireOldInvitations() {
|
|
9935
10497
|
const count = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -9938,8 +10500,8 @@ async function expireOldInvitations() {
|
|
|
9938
10500
|
}
|
|
9939
10501
|
return count;
|
|
9940
10502
|
}
|
|
9941
|
-
async function resendInvitation(
|
|
9942
|
-
const invitation = await invitationsRepository.findById(
|
|
10503
|
+
async function resendInvitation(id15, expiresInDays = 7) {
|
|
10504
|
+
const invitation = await invitationsRepository.findById(id15);
|
|
9943
10505
|
if (!invitation) {
|
|
9944
10506
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
9945
10507
|
}
|
|
@@ -9947,7 +10509,7 @@ async function resendInvitation(id14, expiresInDays = 7) {
|
|
|
9947
10509
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
9948
10510
|
}
|
|
9949
10511
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
9950
|
-
const updated = await invitationsRepository.resend(
|
|
10512
|
+
const updated = await invitationsRepository.resend(id15, newExpiresAt);
|
|
9951
10513
|
if (!updated) {
|
|
9952
10514
|
throw new Error("Failed to update invitation");
|
|
9953
10515
|
}
|
|
@@ -9986,13 +10548,13 @@ async function getAuthSessionService(userId) {
|
|
|
9986
10548
|
// src/server/lib/one-time-token.ts
|
|
9987
10549
|
import { SSETokenManager } from "@spfn/core/event/sse";
|
|
9988
10550
|
var manager = null;
|
|
9989
|
-
function initOneTimeTokenManager(
|
|
10551
|
+
function initOneTimeTokenManager(config3) {
|
|
9990
10552
|
if (manager) {
|
|
9991
10553
|
manager.destroy();
|
|
9992
10554
|
}
|
|
9993
10555
|
manager = new SSETokenManager({
|
|
9994
|
-
ttl:
|
|
9995
|
-
store:
|
|
10556
|
+
ttl: config3?.ttl,
|
|
10557
|
+
store: config3?.store
|
|
9996
10558
|
});
|
|
9997
10559
|
}
|
|
9998
10560
|
function getOneTimeTokenManager() {
|
|
@@ -10104,8 +10666,8 @@ init_repositories();
|
|
|
10104
10666
|
import { env as env12 } from "@spfn/auth/config";
|
|
10105
10667
|
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
10106
10668
|
import {
|
|
10107
|
-
AccountDisabledError as
|
|
10108
|
-
AccountPendingDeletionError as
|
|
10669
|
+
AccountDisabledError as AccountDisabledError3,
|
|
10670
|
+
AccountPendingDeletionError as AccountPendingDeletionError3,
|
|
10109
10671
|
UnverifiedEmailLinkError
|
|
10110
10672
|
} from "@spfn/auth/errors";
|
|
10111
10673
|
|
|
@@ -10140,10 +10702,10 @@ function getDefaultScopes() {
|
|
|
10140
10702
|
}
|
|
10141
10703
|
function getGoogleAuthUrl(state, scopes) {
|
|
10142
10704
|
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
10143
|
-
const
|
|
10705
|
+
const config3 = getGoogleOAuthConfig();
|
|
10144
10706
|
const params = new URLSearchParams({
|
|
10145
|
-
client_id:
|
|
10146
|
-
redirect_uri:
|
|
10707
|
+
client_id: config3.clientId,
|
|
10708
|
+
redirect_uri: config3.redirectUri,
|
|
10147
10709
|
response_type: "code",
|
|
10148
10710
|
scope: resolvedScopes.join(" "),
|
|
10149
10711
|
state,
|
|
@@ -10155,16 +10717,16 @@ function getGoogleAuthUrl(state, scopes) {
|
|
|
10155
10717
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
10156
10718
|
}
|
|
10157
10719
|
async function exchangeCodeForTokens(code) {
|
|
10158
|
-
const
|
|
10720
|
+
const config3 = getGoogleOAuthConfig();
|
|
10159
10721
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
10160
10722
|
method: "POST",
|
|
10161
10723
|
headers: {
|
|
10162
10724
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
10163
10725
|
},
|
|
10164
10726
|
body: new URLSearchParams({
|
|
10165
|
-
client_id:
|
|
10166
|
-
client_secret:
|
|
10167
|
-
redirect_uri:
|
|
10727
|
+
client_id: config3.clientId,
|
|
10728
|
+
client_secret: config3.clientSecret,
|
|
10729
|
+
redirect_uri: config3.redirectUri,
|
|
10168
10730
|
grant_type: "authorization_code",
|
|
10169
10731
|
code
|
|
10170
10732
|
})
|
|
@@ -10188,15 +10750,15 @@ async function getGoogleUserInfo(accessToken) {
|
|
|
10188
10750
|
return response.json();
|
|
10189
10751
|
}
|
|
10190
10752
|
async function refreshAccessToken(refreshToken) {
|
|
10191
|
-
const
|
|
10753
|
+
const config3 = getGoogleOAuthConfig();
|
|
10192
10754
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
10193
10755
|
method: "POST",
|
|
10194
10756
|
headers: {
|
|
10195
10757
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
10196
10758
|
},
|
|
10197
10759
|
body: new URLSearchParams({
|
|
10198
|
-
client_id:
|
|
10199
|
-
client_secret:
|
|
10760
|
+
client_id: config3.clientId,
|
|
10761
|
+
client_secret: config3.clientSecret,
|
|
10200
10762
|
refresh_token: refreshToken,
|
|
10201
10763
|
grant_type: "refresh_token"
|
|
10202
10764
|
})
|
|
@@ -10261,8 +10823,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
10261
10823
|
function registerOAuthProvider(provider) {
|
|
10262
10824
|
registry2.set(provider.id, provider);
|
|
10263
10825
|
}
|
|
10264
|
-
function getOAuthProvider(
|
|
10265
|
-
return registry2.get(
|
|
10826
|
+
function getOAuthProvider(id15) {
|
|
10827
|
+
return registry2.get(id15);
|
|
10266
10828
|
}
|
|
10267
10829
|
function getRegisteredProviders() {
|
|
10268
10830
|
return [...registry2.values()];
|
|
@@ -10383,7 +10945,7 @@ var googleProvider = {
|
|
|
10383
10945
|
registerOAuthProvider(googleProvider);
|
|
10384
10946
|
|
|
10385
10947
|
// src/server/lib/oauth/apple-provider.ts
|
|
10386
|
-
import { createHash as
|
|
10948
|
+
import { createHash as createHash3 } from "crypto";
|
|
10387
10949
|
import { env as env11 } from "@spfn/auth/config";
|
|
10388
10950
|
import { ValidationError as ValidationError4 } from "@spfn/core/errors";
|
|
10389
10951
|
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError2 } from "@spfn/auth/errors";
|
|
@@ -10393,7 +10955,7 @@ function getAppleClientIds() {
|
|
|
10393
10955
|
return (env11.SPFN_AUTH_APPLE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10394
10956
|
}
|
|
10395
10957
|
function hashNonce(rawNonce) {
|
|
10396
|
-
return
|
|
10958
|
+
return createHash3("sha256").update(rawNonce).digest("hex");
|
|
10397
10959
|
}
|
|
10398
10960
|
function unsupportedWebFlow() {
|
|
10399
10961
|
throw new ValidationError4({
|
|
@@ -10516,21 +11078,21 @@ var githubProvider = {
|
|
|
10516
11078
|
return !!(env3.SPFN_AUTH_GITHUB_CLIENT_ID && env3.SPFN_AUTH_GITHUB_CLIENT_SECRET);
|
|
10517
11079
|
},
|
|
10518
11080
|
getAuthUrl(state, scopes) {
|
|
10519
|
-
const
|
|
11081
|
+
const config3 = getGithubConfig();
|
|
10520
11082
|
const params = new URLSearchParams({
|
|
10521
|
-
client_id:
|
|
10522
|
-
redirect_uri:
|
|
11083
|
+
client_id: config3.clientId,
|
|
11084
|
+
redirect_uri: config3.redirectUri,
|
|
10523
11085
|
state,
|
|
10524
11086
|
scope: (scopes ?? getGithubScopes()).join(" ")
|
|
10525
11087
|
});
|
|
10526
11088
|
return `${GITHUB_AUTH_URL}?${params.toString()}`;
|
|
10527
11089
|
},
|
|
10528
11090
|
async exchangeCodeForTokens(code) {
|
|
10529
|
-
const
|
|
11091
|
+
const config3 = getGithubConfig();
|
|
10530
11092
|
return requestGithubTokens(new URLSearchParams({
|
|
10531
|
-
client_id:
|
|
10532
|
-
client_secret:
|
|
10533
|
-
redirect_uri:
|
|
11093
|
+
client_id: config3.clientId,
|
|
11094
|
+
client_secret: config3.clientSecret,
|
|
11095
|
+
redirect_uri: config3.redirectUri,
|
|
10534
11096
|
code
|
|
10535
11097
|
}));
|
|
10536
11098
|
},
|
|
@@ -10560,11 +11122,11 @@ var githubProvider = {
|
|
|
10560
11122
|
};
|
|
10561
11123
|
},
|
|
10562
11124
|
async refreshTokens(refreshToken) {
|
|
10563
|
-
const
|
|
11125
|
+
const config3 = getGithubConfig();
|
|
10564
11126
|
return requestGithubTokens(new URLSearchParams({
|
|
10565
11127
|
grant_type: "refresh_token",
|
|
10566
|
-
client_id:
|
|
10567
|
-
client_secret:
|
|
11128
|
+
client_id: config3.clientId,
|
|
11129
|
+
client_secret: config3.clientSecret,
|
|
10568
11130
|
refresh_token: refreshToken
|
|
10569
11131
|
}));
|
|
10570
11132
|
}
|
|
@@ -10687,26 +11249,26 @@ var kakaoProvider = {
|
|
|
10687
11249
|
return !!env3.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
10688
11250
|
},
|
|
10689
11251
|
getAuthUrl(state, scopes) {
|
|
10690
|
-
const
|
|
11252
|
+
const config3 = getKakaoConfig();
|
|
10691
11253
|
const params = new URLSearchParams({
|
|
10692
11254
|
response_type: "code",
|
|
10693
|
-
client_id:
|
|
10694
|
-
redirect_uri:
|
|
11255
|
+
client_id: config3.clientId,
|
|
11256
|
+
redirect_uri: config3.redirectUri,
|
|
10695
11257
|
state,
|
|
10696
11258
|
scope: (scopes ?? getKakaoScopes()).join(",")
|
|
10697
11259
|
});
|
|
10698
11260
|
return `${KAKAO_AUTH_URL}?${params.toString()}`;
|
|
10699
11261
|
},
|
|
10700
11262
|
async exchangeCodeForTokens(code) {
|
|
10701
|
-
const
|
|
11263
|
+
const config3 = getKakaoConfig();
|
|
10702
11264
|
const params = new URLSearchParams({
|
|
10703
11265
|
grant_type: "authorization_code",
|
|
10704
|
-
client_id:
|
|
10705
|
-
redirect_uri:
|
|
11266
|
+
client_id: config3.clientId,
|
|
11267
|
+
redirect_uri: config3.redirectUri,
|
|
10706
11268
|
code
|
|
10707
11269
|
});
|
|
10708
|
-
if (
|
|
10709
|
-
params.set("client_secret",
|
|
11270
|
+
if (config3.clientSecret) {
|
|
11271
|
+
params.set("client_secret", config3.clientSecret);
|
|
10710
11272
|
}
|
|
10711
11273
|
return requestKakaoTokens(params);
|
|
10712
11274
|
},
|
|
@@ -10748,14 +11310,14 @@ var kakaoProvider = {
|
|
|
10748
11310
|
return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
|
|
10749
11311
|
},
|
|
10750
11312
|
async refreshTokens(refreshToken) {
|
|
10751
|
-
const
|
|
11313
|
+
const config3 = getKakaoConfig();
|
|
10752
11314
|
const params = new URLSearchParams({
|
|
10753
11315
|
grant_type: "refresh_token",
|
|
10754
|
-
client_id:
|
|
11316
|
+
client_id: config3.clientId,
|
|
10755
11317
|
refresh_token: refreshToken
|
|
10756
11318
|
});
|
|
10757
|
-
if (
|
|
10758
|
-
params.set("client_secret",
|
|
11319
|
+
if (config3.clientSecret) {
|
|
11320
|
+
params.set("client_secret", config3.clientSecret);
|
|
10759
11321
|
}
|
|
10760
11322
|
return requestKakaoTokens(params);
|
|
10761
11323
|
},
|
|
@@ -10792,7 +11354,7 @@ registerOAuthProvider(kakaoProvider);
|
|
|
10792
11354
|
init_config();
|
|
10793
11355
|
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
10794
11356
|
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError4 } from "@spfn/auth/errors";
|
|
10795
|
-
import { createDecipheriv, createHash as
|
|
11357
|
+
import { createDecipheriv, createHash as createHash4, createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
10796
11358
|
var NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
|
|
10797
11359
|
var NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
|
|
10798
11360
|
var NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
|
|
@@ -10842,7 +11404,7 @@ async function requestNaverTokens(params) {
|
|
|
10842
11404
|
};
|
|
10843
11405
|
}
|
|
10844
11406
|
function deriveNaverUnlinkKey(clientSecret) {
|
|
10845
|
-
return
|
|
11407
|
+
return createHash4("md5").update(clientSecret).digest().subarray(0, 16);
|
|
10846
11408
|
}
|
|
10847
11409
|
function decodeBase64Url(value) {
|
|
10848
11410
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
@@ -10913,22 +11475,22 @@ var naverProvider = {
|
|
|
10913
11475
|
return !!(env3.SPFN_AUTH_NAVER_CLIENT_ID && env3.SPFN_AUTH_NAVER_CLIENT_SECRET);
|
|
10914
11476
|
},
|
|
10915
11477
|
getAuthUrl(state) {
|
|
10916
|
-
const
|
|
11478
|
+
const config3 = getNaverConfig();
|
|
10917
11479
|
const params = new URLSearchParams({
|
|
10918
11480
|
response_type: "code",
|
|
10919
|
-
client_id:
|
|
10920
|
-
redirect_uri:
|
|
11481
|
+
client_id: config3.clientId,
|
|
11482
|
+
redirect_uri: config3.redirectUri,
|
|
10921
11483
|
state
|
|
10922
11484
|
});
|
|
10923
11485
|
return `${NAVER_AUTH_URL}?${params.toString()}`;
|
|
10924
11486
|
},
|
|
10925
11487
|
async exchangeCodeForTokens(code, options) {
|
|
10926
|
-
const
|
|
11488
|
+
const config3 = getNaverConfig();
|
|
10927
11489
|
return requestNaverTokens(new URLSearchParams({
|
|
10928
11490
|
grant_type: "authorization_code",
|
|
10929
|
-
client_id:
|
|
10930
|
-
client_secret:
|
|
10931
|
-
redirect_uri:
|
|
11491
|
+
client_id: config3.clientId,
|
|
11492
|
+
client_secret: config3.clientSecret,
|
|
11493
|
+
redirect_uri: config3.redirectUri,
|
|
10932
11494
|
code,
|
|
10933
11495
|
state: options.state
|
|
10934
11496
|
}));
|
|
@@ -10969,11 +11531,11 @@ var naverProvider = {
|
|
|
10969
11531
|
return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
|
|
10970
11532
|
},
|
|
10971
11533
|
async refreshTokens(refreshToken) {
|
|
10972
|
-
const
|
|
11534
|
+
const config3 = getNaverConfig();
|
|
10973
11535
|
return requestNaverTokens(new URLSearchParams({
|
|
10974
11536
|
grant_type: "refresh_token",
|
|
10975
|
-
client_id:
|
|
10976
|
-
client_secret:
|
|
11537
|
+
client_id: config3.clientId,
|
|
11538
|
+
client_secret: config3.clientSecret,
|
|
10977
11539
|
refresh_token: refreshToken
|
|
10978
11540
|
}));
|
|
10979
11541
|
},
|
|
@@ -11142,9 +11704,9 @@ async function assertActiveForOAuthSession(userId) {
|
|
|
11142
11704
|
}
|
|
11143
11705
|
if (user.status === "pending_deletion") {
|
|
11144
11706
|
const pending = await getPendingDeletionInfo(user.id);
|
|
11145
|
-
throw new
|
|
11707
|
+
throw new AccountPendingDeletionError3({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
11146
11708
|
}
|
|
11147
|
-
throw new
|
|
11709
|
+
throw new AccountDisabledError3({ status: user.status });
|
|
11148
11710
|
}
|
|
11149
11711
|
async function backfillVerifiedEmail(userId, identity) {
|
|
11150
11712
|
if (!identity.email || !identity.emailVerified) {
|
|
@@ -11287,9 +11849,9 @@ async function oauthUnlinkNotifyService(provider, notification) {
|
|
|
11287
11849
|
}
|
|
11288
11850
|
|
|
11289
11851
|
// src/server/services/oauth-native.service.ts
|
|
11290
|
-
import { runInTransaction as runInTransaction2, onAfterCommit as
|
|
11852
|
+
import { runInTransaction as runInTransaction2, onAfterCommit as onAfterCommit3 } from "@spfn/core/db";
|
|
11291
11853
|
import {
|
|
11292
|
-
InvalidKeyFingerprintError as
|
|
11854
|
+
InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
|
|
11293
11855
|
NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
|
|
11294
11856
|
NonceKeyBindingError
|
|
11295
11857
|
} from "@spfn/auth/errors";
|
|
@@ -11316,7 +11878,7 @@ function assertNonceBindsPublicKey(params) {
|
|
|
11316
11878
|
throw new NonceKeyBindingError();
|
|
11317
11879
|
}
|
|
11318
11880
|
if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
|
|
11319
|
-
throw new
|
|
11881
|
+
throw new InvalidKeyFingerprintError3();
|
|
11320
11882
|
}
|
|
11321
11883
|
}
|
|
11322
11884
|
async function persistNativeLogin(identity, params) {
|
|
@@ -11352,23 +11914,23 @@ async function persistNativeLogin(identity, params) {
|
|
|
11352
11914
|
email: identity.email || void 0,
|
|
11353
11915
|
metadata: params.metadata
|
|
11354
11916
|
};
|
|
11355
|
-
|
|
11917
|
+
onAfterCommit3(() => (isNewUser ? authRegisterEvent : authLoginEvent).emit(eventPayload));
|
|
11356
11918
|
return { userId: String(userId), keyId: params.keyId, isNewUser };
|
|
11357
11919
|
}, { context: "auth:oauth-native" });
|
|
11358
11920
|
}
|
|
11359
11921
|
|
|
11360
11922
|
// src/server/services/ops-token.service.ts
|
|
11361
11923
|
init_ops_tokens_repository();
|
|
11362
|
-
import { createHash as
|
|
11924
|
+
import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
|
|
11363
11925
|
var OPS_TOKEN_PREFIX = "spfn_ops_";
|
|
11364
11926
|
function hashOpsToken(token) {
|
|
11365
|
-
return
|
|
11927
|
+
return createHash5("sha256").update(token).digest("hex");
|
|
11366
11928
|
}
|
|
11367
11929
|
async function issueOpsTokenService(name, scopes, expiresAt) {
|
|
11368
11930
|
if (scopes.length === 0) {
|
|
11369
11931
|
throw new Error("An ops token needs at least one scope ('*' grants all).");
|
|
11370
11932
|
}
|
|
11371
|
-
const token = OPS_TOKEN_PREFIX +
|
|
11933
|
+
const token = OPS_TOKEN_PREFIX + randomBytes2(32).toString("hex");
|
|
11372
11934
|
const record = await opsTokensRepository.create({
|
|
11373
11935
|
name,
|
|
11374
11936
|
tokenHash: hashOpsToken(token),
|
|
@@ -11398,8 +11960,8 @@ async function verifyOpsTokenService(token) {
|
|
|
11398
11960
|
scopes: record.scopes
|
|
11399
11961
|
};
|
|
11400
11962
|
}
|
|
11401
|
-
async function revokeOpsTokenService(
|
|
11402
|
-
return await opsTokensRepository.revokeById(
|
|
11963
|
+
async function revokeOpsTokenService(id15) {
|
|
11964
|
+
return await opsTokensRepository.revokeById(id15);
|
|
11403
11965
|
}
|
|
11404
11966
|
async function listOpsTokensService() {
|
|
11405
11967
|
return await opsTokensRepository.list();
|
|
@@ -11412,7 +11974,7 @@ import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
|
11412
11974
|
|
|
11413
11975
|
// src/server/lib/rate-limit-keys.ts
|
|
11414
11976
|
init_email();
|
|
11415
|
-
import { createHash as
|
|
11977
|
+
import { createHash as createHash6 } from "crypto";
|
|
11416
11978
|
import { getClientIp } from "@spfn/core/middleware";
|
|
11417
11979
|
async function readJsonBody(c) {
|
|
11418
11980
|
try {
|
|
@@ -11448,11 +12010,20 @@ function byIpAndAccount(options = {}) {
|
|
|
11448
12010
|
];
|
|
11449
12011
|
};
|
|
11450
12012
|
}
|
|
12013
|
+
function byIpAndCaller(options = {}) {
|
|
12014
|
+
return async (c) => {
|
|
12015
|
+
const auth = getOptionalAuth(c);
|
|
12016
|
+
return [
|
|
12017
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
12018
|
+
auth ? `caller:${auth.userId}` : void 0
|
|
12019
|
+
];
|
|
12020
|
+
};
|
|
12021
|
+
}
|
|
11451
12022
|
function idTokenKey(body) {
|
|
11452
12023
|
if (typeof body.idToken !== "string" || !body.idToken) {
|
|
11453
12024
|
return void 0;
|
|
11454
12025
|
}
|
|
11455
|
-
return `tok:${
|
|
12026
|
+
return `tok:${createHash6("sha256").update(body.idToken).digest("hex")}`;
|
|
11456
12027
|
}
|
|
11457
12028
|
function byIpAndIdToken(options = {}) {
|
|
11458
12029
|
return async (c) => {
|
|
@@ -11610,6 +12181,69 @@ var login = route.post("/_auth/login").input({
|
|
|
11610
12181
|
const { body } = await c.data();
|
|
11611
12182
|
return await loginService(body);
|
|
11612
12183
|
});
|
|
12184
|
+
var startDeviceAuth = route.post("/_auth/device/start").input({
|
|
12185
|
+
// Bounded, unlike the interceptor bodies the authenticated enrolment
|
|
12186
|
+
// routes use: this is the one place key material arrives from a caller
|
|
12187
|
+
// with nothing to authenticate and is stored before anyone has agreed to
|
|
12188
|
+
// it. Validation refuses an oversize payload before it reaches a row.
|
|
12189
|
+
body: Type.Object({
|
|
12190
|
+
publicKey: PublicKeySchema,
|
|
12191
|
+
keyId: KeyIdSchema,
|
|
12192
|
+
fingerprint: FingerprintSchema,
|
|
12193
|
+
algorithm: Type.Optional(Type.Union(
|
|
12194
|
+
KEY_ALGORITHM.map((algo) => Type.Literal(algo)),
|
|
12195
|
+
{ description: "Signature algorithm" }
|
|
12196
|
+
)),
|
|
12197
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
12198
|
+
platform: Type.Optional(PlatformSchema)
|
|
12199
|
+
})
|
|
12200
|
+
}).use([rateLimitPolicy("auth-device-start", { limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
12201
|
+
const { body } = await c.data();
|
|
12202
|
+
return await startDeviceAuthService(body);
|
|
12203
|
+
});
|
|
12204
|
+
var pollDeviceAuth = route.post("/_auth/device/poll").input({
|
|
12205
|
+
body: Type.Object({
|
|
12206
|
+
deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" })
|
|
12207
|
+
})
|
|
12208
|
+
}).use([rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
12209
|
+
const { body } = await c.data();
|
|
12210
|
+
return await pollDeviceAuthService(body);
|
|
12211
|
+
});
|
|
12212
|
+
var getDeviceAuthInfo = route.post("/_auth/device/info").input({
|
|
12213
|
+
body: Type.Object({
|
|
12214
|
+
userCode: UserCodeSchema
|
|
12215
|
+
})
|
|
12216
|
+
}).use([
|
|
12217
|
+
rateLimitPolicy("auth-device-info", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
|
|
12218
|
+
Transactional()
|
|
12219
|
+
]).handler(async (c) => {
|
|
12220
|
+
const { body } = await c.data();
|
|
12221
|
+
return await getDeviceAuthInfoService(body);
|
|
12222
|
+
});
|
|
12223
|
+
var approveDeviceAuth = route.post("/_auth/device/approve").input({
|
|
12224
|
+
body: Type.Object({
|
|
12225
|
+
userCode: UserCodeSchema
|
|
12226
|
+
})
|
|
12227
|
+
}).use([
|
|
12228
|
+
rateLimitPolicy("auth-device-approve", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
|
|
12229
|
+
Transactional()
|
|
12230
|
+
]).handler(async (c) => {
|
|
12231
|
+
const { body } = await c.data();
|
|
12232
|
+
const { userId } = getAuth(c);
|
|
12233
|
+
return await approveDeviceAuthService({ userCode: body.userCode, userId: Number(userId) });
|
|
12234
|
+
});
|
|
12235
|
+
var denyDeviceAuth = route.post("/_auth/device/deny").input({
|
|
12236
|
+
body: Type.Object({
|
|
12237
|
+
userCode: UserCodeSchema
|
|
12238
|
+
})
|
|
12239
|
+
}).use([
|
|
12240
|
+
rateLimitPolicy("auth-device-deny", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
|
|
12241
|
+
Transactional()
|
|
12242
|
+
]).handler(async (c) => {
|
|
12243
|
+
const { body } = await c.data();
|
|
12244
|
+
await denyDeviceAuthService(body);
|
|
12245
|
+
return c.noContent();
|
|
12246
|
+
});
|
|
11613
12247
|
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
11614
12248
|
const auth = getAuth(c);
|
|
11615
12249
|
if (!auth) {
|
|
@@ -11718,6 +12352,11 @@ var authRouter = defineRouter({
|
|
|
11718
12352
|
verifyCode,
|
|
11719
12353
|
register,
|
|
11720
12354
|
login,
|
|
12355
|
+
startDeviceAuth,
|
|
12356
|
+
pollDeviceAuth,
|
|
12357
|
+
getDeviceAuthInfo,
|
|
12358
|
+
approveDeviceAuth,
|
|
12359
|
+
denyDeviceAuth,
|
|
11721
12360
|
logout,
|
|
11722
12361
|
rotateKey,
|
|
11723
12362
|
listKeys,
|
|
@@ -11732,8 +12371,8 @@ var authRouter = defineRouter({
|
|
|
11732
12371
|
import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
|
|
11733
12372
|
|
|
11734
12373
|
// src/server/middleware/authenticate.ts
|
|
11735
|
-
import { defineMiddleware } from "@spfn/core/route";
|
|
11736
|
-
import { UnauthorizedError as
|
|
12374
|
+
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
12375
|
+
import { UnauthorizedError as UnauthorizedError3 } from "@spfn/core/errors";
|
|
11737
12376
|
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger3, keysRepository as keysRepository3, usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
|
|
11738
12377
|
import {
|
|
11739
12378
|
InvalidTokenError,
|
|
@@ -11742,7 +12381,7 @@ import {
|
|
|
11742
12381
|
} from "@spfn/auth/errors";
|
|
11743
12382
|
|
|
11744
12383
|
// src/server/client-proof/refusal.ts
|
|
11745
|
-
import { randomBytes as
|
|
12384
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
11746
12385
|
|
|
11747
12386
|
// src/server/client-proof/canonical-json.ts
|
|
11748
12387
|
var CanonicalJsonError = class extends Error {
|
|
@@ -11755,13 +12394,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
11755
12394
|
var INT64_MIN = -(2n ** 63n);
|
|
11756
12395
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
11757
12396
|
function parseCanonicalJson(bytes) {
|
|
11758
|
-
let
|
|
12397
|
+
let text15;
|
|
11759
12398
|
try {
|
|
11760
|
-
|
|
12399
|
+
text15 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
11761
12400
|
} catch {
|
|
11762
12401
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
11763
12402
|
}
|
|
11764
|
-
const parser = new Parser(
|
|
12403
|
+
const parser = new Parser(text15);
|
|
11765
12404
|
const value = parser.parseValue();
|
|
11766
12405
|
parser.skipWhitespace();
|
|
11767
12406
|
if (!parser.atEnd()) {
|
|
@@ -11782,8 +12421,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
11782
12421
|
return true;
|
|
11783
12422
|
}
|
|
11784
12423
|
var Parser = class {
|
|
11785
|
-
constructor(
|
|
11786
|
-
this.text =
|
|
12424
|
+
constructor(text15) {
|
|
12425
|
+
this.text = text15;
|
|
11787
12426
|
}
|
|
11788
12427
|
pos = 0;
|
|
11789
12428
|
atEnd() {
|
|
@@ -12100,7 +12739,7 @@ var HTTP_STATUS = {
|
|
|
12100
12739
|
CONTRACT_UNSUPPORTED: 409
|
|
12101
12740
|
};
|
|
12102
12741
|
function newHexId() {
|
|
12103
|
-
return
|
|
12742
|
+
return randomBytes3(16).toString("hex");
|
|
12104
12743
|
}
|
|
12105
12744
|
var ClientProofRefusal = class _ClientProofRefusal {
|
|
12106
12745
|
constructor(code, message) {
|
|
@@ -12202,7 +12841,7 @@ function contractViolation(message) {
|
|
|
12202
12841
|
}
|
|
12203
12842
|
|
|
12204
12843
|
// src/server/client-proof/contract-bundle.ts
|
|
12205
|
-
import { createHash as
|
|
12844
|
+
import { createHash as createHash8 } from "crypto";
|
|
12206
12845
|
import {
|
|
12207
12846
|
CORE_TIME_OPERATION_ID as CORE_TIME_OPERATION_ID2,
|
|
12208
12847
|
ServerTimeResponseSchema
|
|
@@ -12210,7 +12849,7 @@ import {
|
|
|
12210
12849
|
init_types();
|
|
12211
12850
|
|
|
12212
12851
|
// src/server/client-proof/proof.ts
|
|
12213
|
-
import { createHash as
|
|
12852
|
+
import { createHash as createHash7, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
|
|
12214
12853
|
var CLIENT_PROOF_PROFILE = "clientProofV1";
|
|
12215
12854
|
var ABSENT_BODY_SHA256 = "0".repeat(64);
|
|
12216
12855
|
var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
|
|
@@ -12280,7 +12919,7 @@ function verifyClientProof(input, presentedProof, publicKey) {
|
|
|
12280
12919
|
);
|
|
12281
12920
|
}
|
|
12282
12921
|
function sha256Hex(bytes) {
|
|
12283
|
-
return
|
|
12922
|
+
return createHash7("sha256").update(bytes).digest("hex");
|
|
12284
12923
|
}
|
|
12285
12924
|
|
|
12286
12925
|
// src/server/client-proof/admission.ts
|
|
@@ -12372,8 +13011,8 @@ var CORE_PREREQUISITE_OPERATIONS = [
|
|
|
12372
13011
|
|
|
12373
13012
|
// src/server/client-proof/contract-bundle.ts
|
|
12374
13013
|
init_wire_headers();
|
|
12375
|
-
var CONTRACT_VERSION = "0.
|
|
12376
|
-
var CONTRACT_SUPPORTED_RANGE = ">=0.
|
|
13014
|
+
var CONTRACT_VERSION = "0.10.1";
|
|
13015
|
+
var CONTRACT_SUPPORTED_RANGE = ">=0.10.0 <0.11.0";
|
|
12377
13016
|
function required(name, type) {
|
|
12378
13017
|
return { name, type, optional: false };
|
|
12379
13018
|
}
|
|
@@ -12544,7 +13183,7 @@ var CONTRACT_TYPES = [
|
|
|
12544
13183
|
fields: [
|
|
12545
13184
|
required("keyId", "string"),
|
|
12546
13185
|
optional("deviceName", "string"),
|
|
12547
|
-
optional("platform", "
|
|
13186
|
+
optional("platform", "KeyPlatform"),
|
|
12548
13187
|
required("algorithm", "KeyAlgorithm"),
|
|
12549
13188
|
required("fingerprintPrefix", "string"),
|
|
12550
13189
|
required("createdAtMillis", "integer"),
|
|
@@ -12586,10 +13225,94 @@ var CONTRACT_TYPES = [
|
|
|
12586
13225
|
required("revokedCount", "integer"),
|
|
12587
13226
|
required("currentKeyRevoked", "boolean")
|
|
12588
13227
|
]
|
|
13228
|
+
},
|
|
13229
|
+
{
|
|
13230
|
+
name: "StartDeviceAuthRequest",
|
|
13231
|
+
fields: [
|
|
13232
|
+
required("publicKey", "string"),
|
|
13233
|
+
required("keyId", "string"),
|
|
13234
|
+
required("fingerprint", "string"),
|
|
13235
|
+
optional("algorithm", "KeyAlgorithm"),
|
|
13236
|
+
optional("deviceName", "string"),
|
|
13237
|
+
optional("platform", "KeyPlatform")
|
|
13238
|
+
]
|
|
13239
|
+
},
|
|
13240
|
+
{
|
|
13241
|
+
name: "StartDeviceAuthResponse",
|
|
13242
|
+
fields: [
|
|
13243
|
+
required("deviceCode", "string"),
|
|
13244
|
+
required("userCode", "string"),
|
|
13245
|
+
required("expiresAtMillis", "integer"),
|
|
13246
|
+
required("intervalMillis", "integer")
|
|
13247
|
+
]
|
|
13248
|
+
},
|
|
13249
|
+
{
|
|
13250
|
+
name: "PollDeviceAuthRequest",
|
|
13251
|
+
fields: [
|
|
13252
|
+
required("deviceCode", "string")
|
|
13253
|
+
]
|
|
13254
|
+
},
|
|
13255
|
+
/**
|
|
13256
|
+
* The poll union, flattened into the one shape this grammar can carry.
|
|
13257
|
+
*
|
|
13258
|
+
* `status` is the discriminant and the only required field; everything else
|
|
13259
|
+
* belongs to one branch and is therefore optional. `intervalMillis` is the
|
|
13260
|
+
* pending branch, and the five after it are the approved branch — the same
|
|
13261
|
+
* fields `LoginResponse` carries, because an approved poll is the login the
|
|
13262
|
+
* approval produced. `deviceAuthorization.pollStatusRule` states the pairing
|
|
13263
|
+
* the grammar cannot.
|
|
13264
|
+
*/
|
|
13265
|
+
{
|
|
13266
|
+
name: "PollDeviceAuthResponse",
|
|
13267
|
+
fields: [
|
|
13268
|
+
required("status", "DeviceAuthPollStatus"),
|
|
13269
|
+
optional("intervalMillis", "integer"),
|
|
13270
|
+
optional("userId", "string"),
|
|
13271
|
+
optional("publicId", "string"),
|
|
13272
|
+
optional("email", "string"),
|
|
13273
|
+
optional("phone", "string"),
|
|
13274
|
+
optional("passwordChangeRequired", "boolean")
|
|
13275
|
+
]
|
|
13276
|
+
},
|
|
13277
|
+
/**
|
|
13278
|
+
* Info, approve and deny each declare their own request type although all
|
|
13279
|
+
* three carry nothing but `userCode`. An operation's request shape is its
|
|
13280
|
+
* own: a field added to one of them later must not appear on the other two
|
|
13281
|
+
* by accident, which is what a shared type would do.
|
|
13282
|
+
*/
|
|
13283
|
+
{
|
|
13284
|
+
name: "DeviceAuthInfoRequest",
|
|
13285
|
+
fields: [
|
|
13286
|
+
required("userCode", "string")
|
|
13287
|
+
]
|
|
13288
|
+
},
|
|
13289
|
+
{
|
|
13290
|
+
name: "DeviceAuthInfoResponse",
|
|
13291
|
+
fields: [
|
|
13292
|
+
optional("deviceName", "string"),
|
|
13293
|
+
optional("platform", "KeyPlatform"),
|
|
13294
|
+
required("fingerprintPrefix", "string"),
|
|
13295
|
+
required("requestedAtMillis", "integer"),
|
|
13296
|
+
required("expiresAtMillis", "integer")
|
|
13297
|
+
]
|
|
13298
|
+
},
|
|
13299
|
+
{
|
|
13300
|
+
name: "ApproveDeviceAuthRequest",
|
|
13301
|
+
fields: [
|
|
13302
|
+
required("userCode", "string")
|
|
13303
|
+
]
|
|
13304
|
+
},
|
|
13305
|
+
{
|
|
13306
|
+
name: "DenyDeviceAuthRequest",
|
|
13307
|
+
fields: [
|
|
13308
|
+
required("userCode", "string")
|
|
13309
|
+
]
|
|
12589
13310
|
}
|
|
12590
13311
|
];
|
|
12591
13312
|
var CONTRACT_ENUMS = [
|
|
12592
|
-
{ name: "KeyAlgorithm", values: [...KEY_ALGORITHM] }
|
|
13313
|
+
{ name: "KeyAlgorithm", values: [...KEY_ALGORITHM] },
|
|
13314
|
+
{ name: "KeyPlatform", values: [...KEY_PLATFORM] },
|
|
13315
|
+
{ name: "DeviceAuthPollStatus", values: ["pending", "approved"] }
|
|
12593
13316
|
];
|
|
12594
13317
|
var BUNDLE_FILENAME = "spfn-mobile-contract.json";
|
|
12595
13318
|
var BUNDLE_REPO_PATH = `contracts/mobile/${BUNDLE_FILENAME}`;
|
|
@@ -12623,7 +13346,7 @@ import {
|
|
|
12623
13346
|
userProfilesRepository as userProfilesRepository2,
|
|
12624
13347
|
getPendingDeletionInfo as getPendingDeletionInfo2
|
|
12625
13348
|
} from "@spfn/auth/server";
|
|
12626
|
-
import { AccountDisabledError as
|
|
13349
|
+
import { AccountDisabledError as AccountDisabledError4, AccountPendingDeletionError as AccountPendingDeletionError4 } from "@spfn/auth/errors";
|
|
12627
13350
|
|
|
12628
13351
|
// src/server/client-proof/refusal-response.ts
|
|
12629
13352
|
function clientProofRefusalResponse(c, refusal) {
|
|
@@ -12718,9 +13441,9 @@ async function resolveAuthenticatedUser(userId) {
|
|
|
12718
13441
|
if (user.status !== "active") {
|
|
12719
13442
|
if (user.status === "pending_deletion") {
|
|
12720
13443
|
const pending = await getPendingDeletionInfo2(user.id);
|
|
12721
|
-
throw new
|
|
13444
|
+
throw new AccountPendingDeletionError4({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
12722
13445
|
}
|
|
12723
|
-
throw new
|
|
13446
|
+
throw new AccountDisabledError4({ status: user.status });
|
|
12724
13447
|
}
|
|
12725
13448
|
return { user, role: role?.name ?? null, locale };
|
|
12726
13449
|
}
|
|
@@ -12739,6 +13462,9 @@ var ClientProofRefusalError = class extends SerializableError {
|
|
|
12739
13462
|
function refusalError(refusal) {
|
|
12740
13463
|
return new ClientProofRefusalError(refusal);
|
|
12741
13464
|
}
|
|
13465
|
+
function clientProofRefusalOf(err) {
|
|
13466
|
+
return err instanceof ClientProofRefusalError ? err.refusal : null;
|
|
13467
|
+
}
|
|
12742
13468
|
async function runAuthProfile(c) {
|
|
12743
13469
|
try {
|
|
12744
13470
|
const verifier = selectAuthProfile(c);
|
|
@@ -12884,8 +13610,172 @@ function registerAuthProfile(profileId, verifier) {
|
|
|
12884
13610
|
AUTH_PROFILE_VERIFIERS.set(profileId, { verify: verifier.verify.bind(verifier) });
|
|
12885
13611
|
}
|
|
12886
13612
|
|
|
13613
|
+
// src/server/middleware/machine-principals.ts
|
|
13614
|
+
import { decodeProtectedHeader } from "jose";
|
|
13615
|
+
import { defineMiddleware } from "@spfn/core/route";
|
|
13616
|
+
import { ForbiddenError as ForbiddenError2, UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
|
|
13617
|
+
function getMachinePrincipal(c) {
|
|
13618
|
+
return c.get("machinePrincipal") ?? null;
|
|
13619
|
+
}
|
|
13620
|
+
var TOKEN_PREFIX_VERIFIERS = [];
|
|
13621
|
+
var KID_PREFIX_VERIFIERS = [];
|
|
13622
|
+
var REGISTERED_IDS = /* @__PURE__ */ new Set();
|
|
13623
|
+
function readDiscriminator(match) {
|
|
13624
|
+
const tokenPrefix = match?.tokenPrefix;
|
|
13625
|
+
const kidPrefix = match?.kidPrefix;
|
|
13626
|
+
if (tokenPrefix !== void 0 && kidPrefix !== void 0) {
|
|
13627
|
+
return null;
|
|
13628
|
+
}
|
|
13629
|
+
if (typeof tokenPrefix === "string" && tokenPrefix.length > 0) {
|
|
13630
|
+
return { kind: "tokenPrefix", prefix: tokenPrefix };
|
|
13631
|
+
}
|
|
13632
|
+
if (typeof kidPrefix === "string" && kidPrefix.length > 0) {
|
|
13633
|
+
return { kind: "kidPrefix", prefix: kidPrefix };
|
|
13634
|
+
}
|
|
13635
|
+
return null;
|
|
13636
|
+
}
|
|
13637
|
+
function prefixesCollide(a, b) {
|
|
13638
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
13639
|
+
}
|
|
13640
|
+
function registerMachineVerifier(reg) {
|
|
13641
|
+
if (typeof reg?.id !== "string" || reg.id.length === 0) {
|
|
13642
|
+
throw new Error("registerMachineVerifier: id must be a non-empty string");
|
|
13643
|
+
}
|
|
13644
|
+
if (typeof reg.verify !== "function") {
|
|
13645
|
+
throw new Error(`registerMachineVerifier: '${reg.id}' needs a verifier with a callable verify(token, c)`);
|
|
13646
|
+
}
|
|
13647
|
+
const discriminator = readDiscriminator(reg.match);
|
|
13648
|
+
if (discriminator === null) {
|
|
13649
|
+
throw new Error(`registerMachineVerifier: '${reg.id}' needs exactly one non-empty discriminator \u2014 { tokenPrefix } or { kidPrefix }`);
|
|
13650
|
+
}
|
|
13651
|
+
if (REGISTERED_IDS.has(reg.id)) {
|
|
13652
|
+
throw new Error(`registerMachineVerifier: '${reg.id}' is already registered`);
|
|
13653
|
+
}
|
|
13654
|
+
const peers = discriminator.kind === "tokenPrefix" ? TOKEN_PREFIX_VERIFIERS : KID_PREFIX_VERIFIERS;
|
|
13655
|
+
const shadowed = peers.find((peer) => prefixesCollide(peer.prefix, discriminator.prefix));
|
|
13656
|
+
if (shadowed !== void 0) {
|
|
13657
|
+
throw new Error(
|
|
13658
|
+
`registerMachineVerifier: '${reg.id}' ${discriminator.kind} '${discriminator.prefix}' collides with '${shadowed.id}' \u2014 one prefix would shadow the other`
|
|
13659
|
+
);
|
|
13660
|
+
}
|
|
13661
|
+
peers.push({ id: reg.id, prefix: discriminator.prefix, verify: reg.verify.bind(reg) });
|
|
13662
|
+
REGISTERED_IDS.add(reg.id);
|
|
13663
|
+
}
|
|
13664
|
+
function findMachineVerifier(token) {
|
|
13665
|
+
const byTokenPrefix = TOKEN_PREFIX_VERIFIERS.find((entry) => token.startsWith(entry.prefix));
|
|
13666
|
+
if (byTokenPrefix !== void 0) {
|
|
13667
|
+
return byTokenPrefix;
|
|
13668
|
+
}
|
|
13669
|
+
if (KID_PREFIX_VERIFIERS.length === 0) {
|
|
13670
|
+
return null;
|
|
13671
|
+
}
|
|
13672
|
+
const kid = readProtectedKid(token);
|
|
13673
|
+
if (kid === null) {
|
|
13674
|
+
return null;
|
|
13675
|
+
}
|
|
13676
|
+
return KID_PREFIX_VERIFIERS.find((entry) => kid.startsWith(entry.prefix)) ?? null;
|
|
13677
|
+
}
|
|
13678
|
+
function matchesMachineDiscriminator(token) {
|
|
13679
|
+
return findMachineVerifier(token) !== null;
|
|
13680
|
+
}
|
|
13681
|
+
function readProtectedKid(token) {
|
|
13682
|
+
try {
|
|
13683
|
+
const { kid } = decodeProtectedHeader(token);
|
|
13684
|
+
return typeof kid === "string" ? kid : null;
|
|
13685
|
+
} catch {
|
|
13686
|
+
return null;
|
|
13687
|
+
}
|
|
13688
|
+
}
|
|
13689
|
+
var MACHINE_REFUSAL_MESSAGE = "Machine authentication required: Authorization: Bearer <token>";
|
|
13690
|
+
function machineRefusal() {
|
|
13691
|
+
return new UnauthorizedError2({ message: MACHINE_REFUSAL_MESSAGE });
|
|
13692
|
+
}
|
|
13693
|
+
var machineAuth = defineMiddleware("machineAuth", async (c, next) => {
|
|
13694
|
+
const refused = profileChannelRefusal(c);
|
|
13695
|
+
if (refused !== null) {
|
|
13696
|
+
return refused;
|
|
13697
|
+
}
|
|
13698
|
+
const token = extractBearer(c.req.header("Authorization"));
|
|
13699
|
+
if (token === null) {
|
|
13700
|
+
throw machineRefusal();
|
|
13701
|
+
}
|
|
13702
|
+
const entry = findMachineVerifier(token);
|
|
13703
|
+
if (entry === null) {
|
|
13704
|
+
throw machineRefusal();
|
|
13705
|
+
}
|
|
13706
|
+
const principal = await verifiedPrincipal(entry, token, c);
|
|
13707
|
+
if (principal === null) {
|
|
13708
|
+
throw machineRefusal();
|
|
13709
|
+
}
|
|
13710
|
+
c.set("machinePrincipal", principal);
|
|
13711
|
+
await next();
|
|
13712
|
+
return void 0;
|
|
13713
|
+
}, { skips: ["auth"] });
|
|
13714
|
+
function profileChannelRefusal(c) {
|
|
13715
|
+
try {
|
|
13716
|
+
selectAuthProfile(c);
|
|
13717
|
+
return null;
|
|
13718
|
+
} catch (err) {
|
|
13719
|
+
const refusal = clientProofRefusalOf(err);
|
|
13720
|
+
if (refusal === null) {
|
|
13721
|
+
throw err;
|
|
13722
|
+
}
|
|
13723
|
+
return clientProofRefusalResponse(c, refusal);
|
|
13724
|
+
}
|
|
13725
|
+
}
|
|
13726
|
+
async function verifiedPrincipal(entry, token, c) {
|
|
13727
|
+
try {
|
|
13728
|
+
const principal = copyPrincipal(await entry.verify(token, c), entry.id);
|
|
13729
|
+
if (principal === null) {
|
|
13730
|
+
authLogger.middleware.error(`machine verifier '${entry.id}' resolved no principal`);
|
|
13731
|
+
}
|
|
13732
|
+
return principal;
|
|
13733
|
+
} catch (err) {
|
|
13734
|
+
authLogger.middleware.error(`machine verifier '${entry.id}' refused or failed`, err);
|
|
13735
|
+
return null;
|
|
13736
|
+
}
|
|
13737
|
+
}
|
|
13738
|
+
function copyPrincipal(resolved, scheme) {
|
|
13739
|
+
const subjectType = resolved?.subjectType;
|
|
13740
|
+
const subjectId = resolved?.subjectId;
|
|
13741
|
+
const scopes = resolved?.scopes;
|
|
13742
|
+
const claims = resolved?.claims;
|
|
13743
|
+
if (typeof subjectType !== "string" || subjectType.length === 0 || typeof subjectId !== "string" || subjectId.length === 0 || !Array.isArray(scopes)) {
|
|
13744
|
+
return null;
|
|
13745
|
+
}
|
|
13746
|
+
return {
|
|
13747
|
+
subjectType,
|
|
13748
|
+
subjectId,
|
|
13749
|
+
scopes: [...scopes],
|
|
13750
|
+
claims: claims === void 0 ? void 0 : structuredClone(claims),
|
|
13751
|
+
scheme
|
|
13752
|
+
};
|
|
13753
|
+
}
|
|
13754
|
+
var requireMachineScope = defineMiddleware(
|
|
13755
|
+
"machineScope",
|
|
13756
|
+
(...scopes) => async (c, next) => {
|
|
13757
|
+
const principal = getMachinePrincipal(c);
|
|
13758
|
+
if (!principal) {
|
|
13759
|
+
throw machineRefusal();
|
|
13760
|
+
}
|
|
13761
|
+
const granted = new Set(principal.scopes);
|
|
13762
|
+
const missing = scopes.filter((scope) => !granted.has(scope));
|
|
13763
|
+
if (missing.length > 0) {
|
|
13764
|
+
throw new ForbiddenError2({ message: `Machine principal lacks scope: ${missing.join(", ")}` });
|
|
13765
|
+
}
|
|
13766
|
+
await next();
|
|
13767
|
+
}
|
|
13768
|
+
);
|
|
13769
|
+
function extractBearer(header) {
|
|
13770
|
+
if (!header || !header.startsWith("Bearer ")) {
|
|
13771
|
+
return null;
|
|
13772
|
+
}
|
|
13773
|
+
return header.substring(7);
|
|
13774
|
+
}
|
|
13775
|
+
|
|
12887
13776
|
// src/server/middleware/authenticate.ts
|
|
12888
|
-
var
|
|
13777
|
+
var INVALID_TOKEN_MESSAGE = "Invalid token: missing keyId";
|
|
13778
|
+
var authenticate = defineMiddleware2("auth", async (c, next) => {
|
|
12889
13779
|
const profile = await runAuthProfile(c);
|
|
12890
13780
|
if (profile.kind === "refused") {
|
|
12891
13781
|
return profile.response;
|
|
@@ -12901,17 +13791,21 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
12901
13791
|
headers: c.req.header(),
|
|
12902
13792
|
path: c.req.path
|
|
12903
13793
|
});
|
|
12904
|
-
throw new
|
|
13794
|
+
throw new UnauthorizedError3({ message: "Authentication header missing or invalid: Bearer {token}" });
|
|
12905
13795
|
}
|
|
12906
13796
|
const token = authHeader.substring(7);
|
|
13797
|
+
if (matchesMachineDiscriminator(token)) {
|
|
13798
|
+
authLogger3.middleware.warn("Machine credential presented to the user path \u2014 refused", { path: c.req.path });
|
|
13799
|
+
throw new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
|
|
13800
|
+
}
|
|
12907
13801
|
const decoded = decodeToken2(token);
|
|
12908
13802
|
if (!decoded || !decoded.keyId) {
|
|
12909
|
-
throw new
|
|
13803
|
+
throw new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
|
|
12910
13804
|
}
|
|
12911
13805
|
const keyId = decoded.keyId;
|
|
12912
13806
|
const keyRecord = await keysRepository3.findActiveByKeyId(keyId);
|
|
12913
13807
|
if (!keyRecord) {
|
|
12914
|
-
throw new
|
|
13808
|
+
throw new UnauthorizedError3({ message: "Invalid or revoked key" });
|
|
12915
13809
|
}
|
|
12916
13810
|
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
12917
13811
|
throw new KeyExpiredError();
|
|
@@ -12932,7 +13826,7 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
12932
13826
|
throw new InvalidTokenError({ message: "Invalid token signature" });
|
|
12933
13827
|
}
|
|
12934
13828
|
}
|
|
12935
|
-
throw new
|
|
13829
|
+
throw new UnauthorizedError3({ message: "Authentication failed" });
|
|
12936
13830
|
}
|
|
12937
13831
|
const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
|
|
12938
13832
|
keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
|
|
@@ -12958,7 +13852,7 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
12958
13852
|
await next();
|
|
12959
13853
|
return void 0;
|
|
12960
13854
|
});
|
|
12961
|
-
var optionalAuth =
|
|
13855
|
+
var optionalAuth = defineMiddleware2("optionalAuth", async (c, next) => {
|
|
12962
13856
|
const profile = await runAuthProfile(c);
|
|
12963
13857
|
if (profile.kind === "refused") {
|
|
12964
13858
|
return profile.response;
|
|
@@ -12974,6 +13868,10 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
|
12974
13868
|
return void 0;
|
|
12975
13869
|
}
|
|
12976
13870
|
const token = authHeader.substring(7);
|
|
13871
|
+
if (matchesMachineDiscriminator(token)) {
|
|
13872
|
+
authLogger3.middleware.warn("Machine credential presented to the user path \u2014 refused", { path: c.req.path });
|
|
13873
|
+
throw new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
|
|
13874
|
+
}
|
|
12977
13875
|
try {
|
|
12978
13876
|
const decoded = decodeToken2(token);
|
|
12979
13877
|
if (!decoded || !decoded.keyId) {
|
|
@@ -13020,11 +13918,11 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
|
13020
13918
|
}, { skips: ["auth"] });
|
|
13021
13919
|
|
|
13022
13920
|
// src/server/middleware/require-permission.ts
|
|
13023
|
-
import { defineMiddleware as
|
|
13024
|
-
import { ForbiddenError as
|
|
13921
|
+
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
13922
|
+
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
13025
13923
|
import { InsufficientPermissionsError } from "@spfn/auth/errors";
|
|
13026
13924
|
import { getAuth as getAuth2, hasAllPermissions as hasAllPermissions2, hasAnyPermission as hasAnyPermission2, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
13027
|
-
var requirePermissions =
|
|
13925
|
+
var requirePermissions = defineMiddleware3(
|
|
13028
13926
|
"permission",
|
|
13029
13927
|
(...permissionNames) => async (c, next) => {
|
|
13030
13928
|
const auth = getAuth2(c);
|
|
@@ -13033,7 +13931,7 @@ var requirePermissions = defineMiddleware2(
|
|
|
13033
13931
|
permissions: permissionNames,
|
|
13034
13932
|
path: c.req.path
|
|
13035
13933
|
});
|
|
13036
|
-
throw new
|
|
13934
|
+
throw new ForbiddenError3({ message: "Authentication required" });
|
|
13037
13935
|
}
|
|
13038
13936
|
const { userId } = auth;
|
|
13039
13937
|
const allowed = await hasAllPermissions2(userId, permissionNames);
|
|
@@ -13052,7 +13950,7 @@ var requirePermissions = defineMiddleware2(
|
|
|
13052
13950
|
await next();
|
|
13053
13951
|
}
|
|
13054
13952
|
);
|
|
13055
|
-
var requireAnyPermission =
|
|
13953
|
+
var requireAnyPermission = defineMiddleware3(
|
|
13056
13954
|
"anyPermission",
|
|
13057
13955
|
(...permissionNames) => async (c, next) => {
|
|
13058
13956
|
const auth = getAuth2(c);
|
|
@@ -13061,7 +13959,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
13061
13959
|
permissions: permissionNames,
|
|
13062
13960
|
path: c.req.path
|
|
13063
13961
|
});
|
|
13064
|
-
throw new
|
|
13962
|
+
throw new ForbiddenError3({ message: "Authentication required" });
|
|
13065
13963
|
}
|
|
13066
13964
|
const { userId } = auth;
|
|
13067
13965
|
const allowed = await hasAnyPermission2(userId, permissionNames);
|
|
@@ -13082,11 +13980,11 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
13082
13980
|
);
|
|
13083
13981
|
|
|
13084
13982
|
// src/server/middleware/require-role.ts
|
|
13085
|
-
import { defineMiddleware as
|
|
13983
|
+
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
13086
13984
|
import { getAuth as getAuth3, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
13087
|
-
import { ForbiddenError as
|
|
13985
|
+
import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
|
|
13088
13986
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
13089
|
-
var requireRole =
|
|
13987
|
+
var requireRole = defineMiddleware4(
|
|
13090
13988
|
"role",
|
|
13091
13989
|
(...roleNames) => async (c, next) => {
|
|
13092
13990
|
const auth = getAuth3(c);
|
|
@@ -13095,7 +13993,7 @@ var requireRole = defineMiddleware3(
|
|
|
13095
13993
|
roles: roleNames,
|
|
13096
13994
|
path: c.req.path
|
|
13097
13995
|
});
|
|
13098
|
-
throw new
|
|
13996
|
+
throw new ForbiddenError4({ message: "Authentication required" });
|
|
13099
13997
|
}
|
|
13100
13998
|
const { userId, role: userRole } = auth;
|
|
13101
13999
|
if (!userRole || !roleNames.includes(userRole)) {
|
|
@@ -13117,11 +14015,11 @@ var requireRole = defineMiddleware3(
|
|
|
13117
14015
|
);
|
|
13118
14016
|
|
|
13119
14017
|
// src/server/middleware/role-guard.ts
|
|
13120
|
-
import { defineMiddleware as
|
|
14018
|
+
import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
|
|
13121
14019
|
import { getAuth as getAuth4, authLogger as authLogger6 } from "@spfn/auth/server";
|
|
13122
|
-
import { ForbiddenError as
|
|
14020
|
+
import { ForbiddenError as ForbiddenError5 } from "@spfn/core/errors";
|
|
13123
14021
|
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
13124
|
-
var roleGuard =
|
|
14022
|
+
var roleGuard = defineMiddleware5(
|
|
13125
14023
|
"roleGuard",
|
|
13126
14024
|
(options) => async (c, next) => {
|
|
13127
14025
|
const { allow, deny } = options;
|
|
@@ -13133,7 +14031,7 @@ var roleGuard = defineMiddleware4(
|
|
|
13133
14031
|
authLogger6.middleware.warn("Role guard failed: not authenticated", {
|
|
13134
14032
|
path: c.req.path
|
|
13135
14033
|
});
|
|
13136
|
-
throw new
|
|
14034
|
+
throw new ForbiddenError5({ message: "Authentication required" });
|
|
13137
14035
|
}
|
|
13138
14036
|
const { userId, role: userRole } = auth;
|
|
13139
14037
|
if (deny && deny.length > 0) {
|
|
@@ -13169,28 +14067,28 @@ var roleGuard = defineMiddleware4(
|
|
|
13169
14067
|
);
|
|
13170
14068
|
|
|
13171
14069
|
// src/server/middleware/one-time-token-auth.ts
|
|
13172
|
-
import { defineMiddleware as
|
|
13173
|
-
import { UnauthorizedError as
|
|
14070
|
+
import { defineMiddleware as defineMiddleware6 } from "@spfn/core/route";
|
|
14071
|
+
import { UnauthorizedError as UnauthorizedError4 } from "@spfn/core/errors";
|
|
13174
14072
|
import { usersRepository as usersRepository4, userProfilesRepository as userProfilesRepository4 } from "@spfn/auth/server";
|
|
13175
|
-
var oneTimeTokenAuth =
|
|
14073
|
+
var oneTimeTokenAuth = defineMiddleware6("oneTimeTokenAuth", async (c, next) => {
|
|
13176
14074
|
const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
|
|
13177
14075
|
if (!token) {
|
|
13178
|
-
throw new
|
|
14076
|
+
throw new UnauthorizedError4({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
|
|
13179
14077
|
}
|
|
13180
14078
|
const userId = await verifyOneTimeTokenService(token);
|
|
13181
14079
|
if (!userId) {
|
|
13182
|
-
throw new
|
|
14080
|
+
throw new UnauthorizedError4({ message: "Invalid or expired one-time token" });
|
|
13183
14081
|
}
|
|
13184
14082
|
const [result, locale] = await Promise.all([
|
|
13185
14083
|
usersRepository4.findByIdWithRole(Number(userId)),
|
|
13186
14084
|
userProfilesRepository4.findLocaleByUserId(Number(userId))
|
|
13187
14085
|
]);
|
|
13188
14086
|
if (!result) {
|
|
13189
|
-
throw new
|
|
14087
|
+
throw new UnauthorizedError4({ message: "User not found" });
|
|
13190
14088
|
}
|
|
13191
14089
|
const { user, role } = result;
|
|
13192
14090
|
if (user.status !== "active") {
|
|
13193
|
-
throw new
|
|
14091
|
+
throw new UnauthorizedError4({ message: "Account is not active" });
|
|
13194
14092
|
}
|
|
13195
14093
|
c.set("auth", {
|
|
13196
14094
|
user,
|
|
@@ -13211,39 +14109,39 @@ function extractOTTHeader(header) {
|
|
|
13211
14109
|
}
|
|
13212
14110
|
|
|
13213
14111
|
// src/server/middleware/ops-token-auth.ts
|
|
13214
|
-
import { defineMiddleware as
|
|
13215
|
-
import { ForbiddenError as
|
|
14112
|
+
import { defineMiddleware as defineMiddleware7 } from "@spfn/core/route";
|
|
14113
|
+
import { ForbiddenError as ForbiddenError6, UnauthorizedError as UnauthorizedError5 } from "@spfn/core/errors";
|
|
13216
14114
|
function getOpsToken(c) {
|
|
13217
14115
|
return c.get("opsToken") ?? null;
|
|
13218
14116
|
}
|
|
13219
|
-
var opsTokenAuth =
|
|
13220
|
-
const token =
|
|
14117
|
+
var opsTokenAuth = defineMiddleware7("opsTokenAuth", async (c, next) => {
|
|
14118
|
+
const token = extractBearer2(c.req.header("Authorization"));
|
|
13221
14119
|
if (!token) {
|
|
13222
|
-
throw new
|
|
14120
|
+
throw new UnauthorizedError5({ message: "Ops token required: Authorization: Bearer <token>" });
|
|
13223
14121
|
}
|
|
13224
14122
|
const verified = await verifyOpsTokenService(token);
|
|
13225
14123
|
if (!verified) {
|
|
13226
|
-
throw new
|
|
14124
|
+
throw new UnauthorizedError5({ message: "Invalid ops token" });
|
|
13227
14125
|
}
|
|
13228
14126
|
c.set("opsToken", verified);
|
|
13229
14127
|
await next();
|
|
13230
14128
|
}, { skips: ["auth"] });
|
|
13231
|
-
var requireOpsScope =
|
|
14129
|
+
var requireOpsScope = defineMiddleware7(
|
|
13232
14130
|
"opsScope",
|
|
13233
14131
|
(...scopes) => async (c, next) => {
|
|
13234
14132
|
const token = getOpsToken(c);
|
|
13235
14133
|
if (!token) {
|
|
13236
|
-
throw new
|
|
14134
|
+
throw new UnauthorizedError5({ message: "Ops token required: Authorization: Bearer <token>" });
|
|
13237
14135
|
}
|
|
13238
14136
|
const granted = new Set(token.scopes);
|
|
13239
14137
|
const missing = scopes.filter((scope) => !granted.has(scope) && !granted.has("*"));
|
|
13240
14138
|
if (missing.length > 0) {
|
|
13241
|
-
throw new
|
|
14139
|
+
throw new ForbiddenError6({ message: `Ops token lacks scope: ${missing.join(", ")}` });
|
|
13242
14140
|
}
|
|
13243
14141
|
await next();
|
|
13244
14142
|
}
|
|
13245
14143
|
);
|
|
13246
|
-
function
|
|
14144
|
+
function extractBearer2(header) {
|
|
13247
14145
|
if (!header || !header.startsWith("Bearer ")) {
|
|
13248
14146
|
return null;
|
|
13249
14147
|
}
|
|
@@ -14038,7 +14936,7 @@ var oauthRouter = defineRouter4({
|
|
|
14038
14936
|
|
|
14039
14937
|
// src/server/routes/admin/index.ts
|
|
14040
14938
|
init_esm();
|
|
14041
|
-
import { ForbiddenError as
|
|
14939
|
+
import { ForbiddenError as ForbiddenError7 } from "@spfn/core/errors";
|
|
14042
14940
|
import { route as route5 } from "@spfn/core/route";
|
|
14043
14941
|
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
14044
14942
|
query: Type.Object({
|
|
@@ -14108,11 +15006,11 @@ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
|
14108
15006
|
const { params, body } = await c.data();
|
|
14109
15007
|
const auth = getAuth(c);
|
|
14110
15008
|
if (params.userId === Number(auth.userId)) {
|
|
14111
|
-
throw new
|
|
15009
|
+
throw new ForbiddenError7({ message: "Cannot change your own role" });
|
|
14112
15010
|
}
|
|
14113
15011
|
const targetRole = await getUserRole(params.userId);
|
|
14114
15012
|
if (targetRole === "superadmin") {
|
|
14115
|
-
throw new
|
|
15013
|
+
throw new ForbiddenError7({ message: "Cannot modify superadmin role" });
|
|
14116
15014
|
}
|
|
14117
15015
|
await assertCanAssignRole(auth.userId, body.roleId);
|
|
14118
15016
|
await updateUserService(params.userId, { roleId: body.roleId });
|
|
@@ -14236,6 +15134,12 @@ var mainAuthRouter = defineRouter6({
|
|
|
14236
15134
|
confirmSignupLink,
|
|
14237
15135
|
completeSignup,
|
|
14238
15136
|
login,
|
|
15137
|
+
// Device-code login routes
|
|
15138
|
+
startDeviceAuth,
|
|
15139
|
+
pollDeviceAuth,
|
|
15140
|
+
getDeviceAuthInfo,
|
|
15141
|
+
approveDeviceAuth,
|
|
15142
|
+
denyDeviceAuth,
|
|
14239
15143
|
logout,
|
|
14240
15144
|
rotateKey,
|
|
14241
15145
|
listKeys,
|
|
@@ -14479,15 +15383,64 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
14479
15383
|
return hoursRemaining < thresholdHours;
|
|
14480
15384
|
}
|
|
14481
15385
|
|
|
14482
|
-
// src/server/
|
|
15386
|
+
// src/server/lib/csrf.ts
|
|
14483
15387
|
import { env as env14 } from "@spfn/auth/config";
|
|
15388
|
+
var CSRF_HEADER = "x-spfn-csrf";
|
|
15389
|
+
var CSRF_SUBKEY_LABEL = "spfn-auth-csrf-token-v1";
|
|
15390
|
+
var MAX_CANDIDATES = 32;
|
|
15391
|
+
function sessionSecret() {
|
|
15392
|
+
const secret = env14.SPFN_AUTH_SESSION_SECRET;
|
|
15393
|
+
if (!secret) {
|
|
15394
|
+
throw new Error(
|
|
15395
|
+
"SPFN_AUTH_SESSION_SECRET is required for CSRF protection. Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off."
|
|
15396
|
+
);
|
|
15397
|
+
}
|
|
15398
|
+
return secret;
|
|
15399
|
+
}
|
|
15400
|
+
async function hmacSha256(key, message) {
|
|
15401
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
15402
|
+
"raw",
|
|
15403
|
+
key.buffer,
|
|
15404
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
15405
|
+
false,
|
|
15406
|
+
["sign"]
|
|
15407
|
+
);
|
|
15408
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
|
|
15409
|
+
return new Uint8Array(signature);
|
|
15410
|
+
}
|
|
15411
|
+
function toHex(bytes) {
|
|
15412
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
15413
|
+
}
|
|
15414
|
+
async function deriveCsrfToken(keyId) {
|
|
15415
|
+
const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);
|
|
15416
|
+
return toHex(await hmacSha256(subkey, keyId));
|
|
15417
|
+
}
|
|
15418
|
+
function timingSafeEqualString(a, b) {
|
|
15419
|
+
if (a.length !== b.length) {
|
|
15420
|
+
return false;
|
|
15421
|
+
}
|
|
15422
|
+
let difference = 0;
|
|
15423
|
+
for (let i = 0; i < a.length; i++) {
|
|
15424
|
+
difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
15425
|
+
}
|
|
15426
|
+
return difference === 0;
|
|
15427
|
+
}
|
|
15428
|
+
function matchesCsrfToken(expected, presented) {
|
|
15429
|
+
if (!presented) {
|
|
15430
|
+
return false;
|
|
15431
|
+
}
|
|
15432
|
+
return presented.split(",", MAX_CANDIDATES).some((candidate) => timingSafeEqualString(expected, candidate.trim()));
|
|
15433
|
+
}
|
|
15434
|
+
|
|
15435
|
+
// src/server/setup.ts
|
|
15436
|
+
import { env as env15 } from "@spfn/auth/config";
|
|
14484
15437
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
14485
15438
|
init_repositories();
|
|
14486
15439
|
function parseAdminAccounts() {
|
|
14487
15440
|
const accounts = [];
|
|
14488
|
-
if (
|
|
15441
|
+
if (env15.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
14489
15442
|
try {
|
|
14490
|
-
const accountsJson =
|
|
15443
|
+
const accountsJson = env15.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
14491
15444
|
const parsed = JSON.parse(accountsJson);
|
|
14492
15445
|
if (!Array.isArray(parsed)) {
|
|
14493
15446
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -14514,11 +15467,11 @@ function parseAdminAccounts() {
|
|
|
14514
15467
|
return accounts;
|
|
14515
15468
|
}
|
|
14516
15469
|
}
|
|
14517
|
-
const adminEmails =
|
|
15470
|
+
const adminEmails = env15.SPFN_AUTH_ADMIN_EMAILS;
|
|
14518
15471
|
if (adminEmails) {
|
|
14519
15472
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
14520
|
-
const passwords = (
|
|
14521
|
-
const roles2 = (
|
|
15473
|
+
const passwords = (env15.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
15474
|
+
const roles2 = (env15.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
14522
15475
|
if (passwords.length !== emails.length) {
|
|
14523
15476
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
14524
15477
|
return accounts;
|
|
@@ -14540,8 +15493,8 @@ function parseAdminAccounts() {
|
|
|
14540
15493
|
}
|
|
14541
15494
|
return accounts;
|
|
14542
15495
|
}
|
|
14543
|
-
const adminEmail =
|
|
14544
|
-
const adminPassword =
|
|
15496
|
+
const adminEmail = env15.SPFN_AUTH_ADMIN_EMAIL;
|
|
15497
|
+
const adminPassword = env15.SPFN_AUTH_ADMIN_PASSWORD;
|
|
14545
15498
|
if (adminEmail && adminPassword) {
|
|
14546
15499
|
accounts.push({
|
|
14547
15500
|
email: adminEmail,
|
|
@@ -14602,19 +15555,88 @@ async function ensureAdminExists() {
|
|
|
14602
15555
|
}
|
|
14603
15556
|
}
|
|
14604
15557
|
|
|
15558
|
+
// src/server/lib/oauth/redirect-uri-check.ts
|
|
15559
|
+
var PROVIDERS = ["google", "kakao", "naver", "github"];
|
|
15560
|
+
var OPT_OUT_VAR = "SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK";
|
|
15561
|
+
function redirectUriVar(provider) {
|
|
15562
|
+
return `SPFN_AUTH_${provider.toUpperCase()}_REDIRECT_URI`;
|
|
15563
|
+
}
|
|
15564
|
+
function resolveWebAppOrigin(env16) {
|
|
15565
|
+
const configured2 = env16.NEXT_PUBLIC_SPFN_APP_URL || env16.SPFN_APP_URL;
|
|
15566
|
+
if (!configured2) {
|
|
15567
|
+
return null;
|
|
15568
|
+
}
|
|
15569
|
+
try {
|
|
15570
|
+
return new URL(configured2).origin;
|
|
15571
|
+
} catch {
|
|
15572
|
+
return null;
|
|
15573
|
+
}
|
|
15574
|
+
}
|
|
15575
|
+
function conformanceDetail(value, webAppOrigin, callbackPath) {
|
|
15576
|
+
let url;
|
|
15577
|
+
try {
|
|
15578
|
+
url = new URL(value);
|
|
15579
|
+
} catch {
|
|
15580
|
+
return " The value is not a URL.";
|
|
15581
|
+
}
|
|
15582
|
+
if (url.origin !== webAppOrigin || url.pathname !== callbackPath) {
|
|
15583
|
+
return "";
|
|
15584
|
+
}
|
|
15585
|
+
if (url.search !== "" || url.hash !== "") {
|
|
15586
|
+
return " The value carries a query string or fragment; the callback URL is the path alone.";
|
|
15587
|
+
}
|
|
15588
|
+
return null;
|
|
15589
|
+
}
|
|
15590
|
+
function providerRefusal(provider, env16, webAppOrigin) {
|
|
15591
|
+
const variable = redirectUriVar(provider);
|
|
15592
|
+
const value = env16[variable];
|
|
15593
|
+
if (!value) {
|
|
15594
|
+
return null;
|
|
15595
|
+
}
|
|
15596
|
+
const callbackPath = `/_auth/oauth/${provider}/callback`;
|
|
15597
|
+
const detail = conformanceDetail(value, webAppOrigin, callbackPath);
|
|
15598
|
+
if (detail === null) {
|
|
15599
|
+
return null;
|
|
15600
|
+
}
|
|
15601
|
+
return `${variable} must be on the web app origin (${webAppOrigin}) at ${callbackPath}: the callback's CSRF cookie is host-only and /_auth/* is forwarded to the API by the app's rewrite. Unset it to use the default, fix the origin, or set ${OPT_OUT_VAR}=off for a deployment that deliberately terminates the callback elsewhere.${detail}`;
|
|
15602
|
+
}
|
|
15603
|
+
function assertOAuthRedirectUris(env16 = process.env) {
|
|
15604
|
+
const examined = PROVIDERS.map(redirectUriVar).join(", ");
|
|
15605
|
+
if (env16[OPT_OUT_VAR] === "off") {
|
|
15606
|
+
authLogger.service.info(
|
|
15607
|
+
`${OPT_OUT_VAR}=off: the OAuth callback origin check is disabled. Not examined: ${examined}.`
|
|
15608
|
+
);
|
|
15609
|
+
return;
|
|
15610
|
+
}
|
|
15611
|
+
const webAppOrigin = resolveWebAppOrigin(env16);
|
|
15612
|
+
if (!webAppOrigin) {
|
|
15613
|
+
authLogger.service.info(
|
|
15614
|
+
`The OAuth callback origin check was skipped: neither NEXT_PUBLIC_SPFN_APP_URL nor SPFN_APP_URL resolves to a parseable URL, which the startup env validation reports on its own. Not examined: ${examined}.`
|
|
15615
|
+
);
|
|
15616
|
+
return;
|
|
15617
|
+
}
|
|
15618
|
+
const refusals = PROVIDERS.map((provider) => providerRefusal(provider, env16, webAppOrigin)).filter((refusal) => refusal !== null);
|
|
15619
|
+
if (refusals.length > 0) {
|
|
15620
|
+
throw new Error(refusals.join("\n"));
|
|
15621
|
+
}
|
|
15622
|
+
}
|
|
15623
|
+
|
|
14605
15624
|
// src/server/lifecycle.ts
|
|
14606
15625
|
function createAuthLifecycle(options = {}) {
|
|
14607
15626
|
configureDeletion(options.deletion);
|
|
15627
|
+
configureDeviceAuth(options.deviceAuth);
|
|
14608
15628
|
return {
|
|
14609
15629
|
/**
|
|
14610
15630
|
* Initialize auth system after database is ready
|
|
14611
15631
|
*
|
|
14612
15632
|
* Performs:
|
|
15633
|
+
* 0. Refuses boot on an OAuth redirect URI override off the web app origin
|
|
14613
15634
|
* 1. Ensures admin account exists (creates if missing)
|
|
14614
15635
|
* 2. Initializes RBAC system with built-in + custom roles/permissions
|
|
14615
15636
|
* 3. Initializes one-time token manager
|
|
14616
15637
|
*/
|
|
14617
15638
|
afterInfrastructure: async () => {
|
|
15639
|
+
assertOAuthRedirectUris();
|
|
14618
15640
|
await initializeAuth(options);
|
|
14619
15641
|
try {
|
|
14620
15642
|
await normalizeStoredEmails();
|
|
@@ -14653,20 +15675,28 @@ export {
|
|
|
14653
15675
|
AuthMetadataRepository,
|
|
14654
15676
|
AuthProviderSchema,
|
|
14655
15677
|
COOKIE_NAMES,
|
|
15678
|
+
CSRF_HEADER,
|
|
14656
15679
|
DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE,
|
|
14657
15680
|
DEFAULT_DELETION_GRACE_PERIOD_DAYS,
|
|
14658
15681
|
DEFAULT_DELETION_PURGE_CRON,
|
|
14659
15682
|
DEFAULT_DELETION_PURGE_STRATEGY,
|
|
14660
15683
|
DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
15684
|
+
DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
15685
|
+
DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
15686
|
+
DEVICE_AUTH_STATUSES,
|
|
15687
|
+
DeviceAuthPollResponseSchema,
|
|
15688
|
+
DeviceAuthorizationsRepository,
|
|
14661
15689
|
DeviceNameSchema,
|
|
14662
15690
|
EmailSchema,
|
|
14663
15691
|
EnvironmentKeyringTokenCipher,
|
|
15692
|
+
FingerprintSchema,
|
|
14664
15693
|
INVITATION_STATUSES,
|
|
14665
15694
|
InvitationsRepository,
|
|
14666
15695
|
KEY_ALGORITHM,
|
|
14667
15696
|
KEY_DEVICE_NAME_MAX_LENGTH,
|
|
14668
15697
|
KEY_FINGERPRINT_PREFIX_LENGTH,
|
|
14669
15698
|
KEY_PLATFORM,
|
|
15699
|
+
KeyIdSchema,
|
|
14670
15700
|
KeysRepository,
|
|
14671
15701
|
OpsTokensRepository,
|
|
14672
15702
|
PURGE_STRATEGIES,
|
|
@@ -14674,14 +15704,18 @@ export {
|
|
|
14674
15704
|
PermissionsRepository,
|
|
14675
15705
|
PhoneSchema,
|
|
14676
15706
|
PlatformSchema,
|
|
15707
|
+
PublicKeySchema,
|
|
14677
15708
|
RolePermissionsRepository,
|
|
14678
15709
|
RolesRepository,
|
|
14679
15710
|
SOCIAL_PROVIDERS,
|
|
14680
15711
|
SignupLinkTokensRepository,
|
|
14681
15712
|
SocialAccountsRepository,
|
|
14682
15713
|
TargetTypeSchema,
|
|
15714
|
+
USER_CODE_ALPHABET,
|
|
15715
|
+
USER_CODE_LENGTH,
|
|
14683
15716
|
USER_STATUSES,
|
|
14684
15717
|
UnlinkNotifyRejection,
|
|
15718
|
+
UserCodeSchema,
|
|
14685
15719
|
UserPermissionsRepository,
|
|
14686
15720
|
UserProfilesRepository,
|
|
14687
15721
|
UsersRepository,
|
|
@@ -14694,7 +15728,9 @@ export {
|
|
|
14694
15728
|
accountDeletionRequestsRepository,
|
|
14695
15729
|
addPermissionToRole,
|
|
14696
15730
|
appleProvider,
|
|
15731
|
+
approveDeviceAuthService,
|
|
14697
15732
|
assertCanAssignRole,
|
|
15733
|
+
assertKeyMatchesAlgorithm,
|
|
14698
15734
|
authDeletionCancelledEvent,
|
|
14699
15735
|
authDeletionCompletedEvent,
|
|
14700
15736
|
authDeletionRequestedEvent,
|
|
@@ -14715,6 +15751,7 @@ export {
|
|
|
14715
15751
|
completeSignupService,
|
|
14716
15752
|
configureAuth,
|
|
14717
15753
|
configureDeletion,
|
|
15754
|
+
configureDeviceAuth,
|
|
14718
15755
|
configureOAuthTokenCipher,
|
|
14719
15756
|
confirmSignupLinkService,
|
|
14720
15757
|
createAuthDeletionJobRouter,
|
|
@@ -14727,20 +15764,31 @@ export {
|
|
|
14727
15764
|
decryptToken,
|
|
14728
15765
|
deleteInvitation,
|
|
14729
15766
|
deleteRole,
|
|
15767
|
+
denyDeviceAuthService,
|
|
15768
|
+
deriveCsrfToken,
|
|
15769
|
+
deviceAuthorizations,
|
|
15770
|
+
deviceAuthorizationsRepository,
|
|
14730
15771
|
encryptToken,
|
|
14731
15772
|
exchangeCodeForTokens,
|
|
14732
15773
|
expireOldInvitations,
|
|
15774
|
+
formatUserCode,
|
|
14733
15775
|
generateClientToken,
|
|
15776
|
+
generateDeviceCode,
|
|
14734
15777
|
generateKeyPair,
|
|
14735
15778
|
generateKeyPairES256,
|
|
14736
15779
|
generateKeyPairRS256,
|
|
14737
15780
|
generateOAuthNonce,
|
|
14738
15781
|
generateToken,
|
|
15782
|
+
generateUserCode,
|
|
14739
15783
|
getAllRoles,
|
|
14740
15784
|
getAuth,
|
|
14741
15785
|
getAuthConfig,
|
|
14742
15786
|
getAuthSessionService,
|
|
15787
|
+
getCsrfExemptPaths,
|
|
15788
|
+
getCsrfMode,
|
|
14743
15789
|
getDeletionConfig,
|
|
15790
|
+
getDeviceAuthConfig,
|
|
15791
|
+
getDeviceAuthInfoService,
|
|
14744
15792
|
getDummyPasswordHash,
|
|
14745
15793
|
getEnabledOAuthProviders,
|
|
14746
15794
|
getGoogleAccessToken,
|
|
@@ -14752,6 +15800,7 @@ export {
|
|
|
14752
15800
|
getKeyId,
|
|
14753
15801
|
getKeySize,
|
|
14754
15802
|
getLocale,
|
|
15803
|
+
getMachinePrincipal,
|
|
14755
15804
|
getOAuthProvider,
|
|
14756
15805
|
getOneTimeTokenManager,
|
|
14757
15806
|
getOpsToken,
|
|
@@ -14778,6 +15827,7 @@ export {
|
|
|
14778
15827
|
hasAnyRole,
|
|
14779
15828
|
hasPermission,
|
|
14780
15829
|
hasRole,
|
|
15830
|
+
hashDeviceCode,
|
|
14781
15831
|
hashPassword,
|
|
14782
15832
|
initOneTimeTokenManager,
|
|
14783
15833
|
initializeAuth,
|
|
@@ -14797,11 +15847,14 @@ export {
|
|
|
14797
15847
|
listOpsTokensService,
|
|
14798
15848
|
loginService,
|
|
14799
15849
|
logoutService,
|
|
15850
|
+
machineAuth,
|
|
14800
15851
|
matchOAuthCsrfCookies,
|
|
15852
|
+
matchesCsrfToken,
|
|
14801
15853
|
naverProvider,
|
|
14802
15854
|
normalizeEmail,
|
|
14803
15855
|
normalizeOptionalEmail,
|
|
14804
15856
|
normalizeStoredEmails,
|
|
15857
|
+
normalizeUserCode,
|
|
14805
15858
|
oauthCallbackService,
|
|
14806
15859
|
oauthNativeService,
|
|
14807
15860
|
oauthStartService,
|
|
@@ -14815,9 +15868,11 @@ export {
|
|
|
14815
15868
|
parseDuration,
|
|
14816
15869
|
permissions,
|
|
14817
15870
|
permissionsRepository,
|
|
15871
|
+
pollDeviceAuthService,
|
|
14818
15872
|
purgeUserService,
|
|
14819
15873
|
refreshAccessToken,
|
|
14820
15874
|
registerAuthProfile,
|
|
15875
|
+
registerMachineVerifier,
|
|
14821
15876
|
registerOAuthProvider,
|
|
14822
15877
|
registerPublicKeyService,
|
|
14823
15878
|
registerService,
|
|
@@ -14826,6 +15881,7 @@ export {
|
|
|
14826
15881
|
requestSignupLinkService,
|
|
14827
15882
|
requireAnyPermission,
|
|
14828
15883
|
requireEnabledProvider,
|
|
15884
|
+
requireMachineScope,
|
|
14829
15885
|
requireOpsScope,
|
|
14830
15886
|
requirePermissions,
|
|
14831
15887
|
requireRole,
|
|
@@ -14851,6 +15907,7 @@ export {
|
|
|
14851
15907
|
signupLinkTokens,
|
|
14852
15908
|
signupLinkTokensRepository,
|
|
14853
15909
|
socialAccountsRepository,
|
|
15910
|
+
startDeviceAuthService,
|
|
14854
15911
|
sweepDuePurges,
|
|
14855
15912
|
unsealSession,
|
|
14856
15913
|
updateLastLoginService,
|